import { type WsFilePath, WsPath } from './ws-path';

const EXPLICIT_SCHEME_RE = /^[a-z][a-z\d+.-]*:/i;
export const EMBEDDABLE_IMAGE_EXTENSIONS = [
  '.avif',
  '.gif',
  '.jpg',
  '.jpeg',
  '.png',
  '.svg',
  '.webp',
] as const;

export type EmbeddableWorkspaceAssetKind = 'image';

const EMBEDDABLE_IMAGE_EXTENSION_SET = new Set<string>(
  EMBEDDABLE_IMAGE_EXTENSIONS,
);

function decodeSafeSegment(value: string): string | undefined {
  if (!value || value.includes('\\')) {
    return undefined;
  }

  try {
    const decoded = decodeURIComponent(value);
    return decoded &&
      !decoded.includes('/') &&
      !decoded.includes('\\') &&
      !decoded.includes('\0')
      ? decoded
      : undefined;
  } catch {
    return undefined;
  }
}

function encodeSegment(value: string): string {
  return encodeURIComponent(value);
}

export function isEmbeddableImageExtension(extension: string): boolean {
  return EMBEDDABLE_IMAGE_EXTENSION_SET.has(extension.toLowerCase());
}

/**
 * Categories of workspace assets the app can preview inline on the asset page
 * using only native browser elements (`<img>`, `<iframe>`, `<video>`,
 * `<audio>`, `<pre>`). Anything outside these kinds falls back to a download
 * affordance. Keep this list conservative: only formats browsers render without
 * a third-party viewer belong here.
 */
export type AssetPreviewKind = 'image' | 'pdf' | 'video' | 'audio' | 'text';

const ASSET_PREVIEW_EXTENSIONS: Record<AssetPreviewKind, readonly string[]> = {
  // Reuse the embeddable image set and add a couple of formats browsers render
  // in `<img>` even though they are not offered for Markdown embedding.
  image: [...EMBEDDABLE_IMAGE_EXTENSIONS, '.bmp', '.ico'],
  pdf: ['.pdf'],
  video: ['.m4v', '.mov', '.mp4', '.ogv', '.webm'],
  audio: [
    '.aac',
    '.flac',
    '.m4a',
    '.oga',
    '.ogg',
    '.opus',
    '.mp3',
    '.wav',
    '.weba',
  ],
  // Plain-text formats rendered verbatim in a `<pre>`; no syntax highlighting.
  text: [
    '.bash',
    '.c',
    '.cfg',
    '.conf',
    '.cpp',
    '.css',
    '.csv',
    '.env',
    '.go',
    '.h',
    '.htm',
    '.html',
    '.ini',
    '.java',
    '.js',
    '.json',
    '.jsonc',
    '.jsx',
    '.less',
    '.log',
    '.mjs',
    '.cjs',
    '.py',
    '.rb',
    '.rs',
    '.scss',
    '.sh',
    '.sql',
    '.text',
    '.toml',
    '.ts',
    '.tsx',
    '.tsv',
    '.txt',
    '.xml',
    '.yaml',
    '.yml',
    '.zsh',
  ],
};

const ASSET_PREVIEW_KIND_BY_EXTENSION = new Map<string, AssetPreviewKind>();
for (const [kind, extensions] of Object.entries(ASSET_PREVIEW_EXTENSIONS) as [
  AssetPreviewKind,
  readonly string[],
][]) {
  for (const extension of extensions) {
    ASSET_PREVIEW_KIND_BY_EXTENSION.set(extension, kind);
  }
}

/**
 * Maps a file extension (with or without a leading dot, any case) to the way it
 * can be previewed inline, or `undefined` when the app cannot render it.
 */
export function assetPreviewKindForExtension(
  extension: string | undefined,
): AssetPreviewKind | undefined {
  if (!extension) {
    return undefined;
  }
  const lower = extension.toLowerCase();
  const normalized = lower.startsWith('.') ? lower : `.${lower}`;
  return ASSET_PREVIEW_KIND_BY_EXTENSION.get(normalized);
}

/**
 * Resolves how a workspace asset can be previewed inline based on its
 * extension, or `undefined` when only a download fallback is possible.
 */
export function getAssetPreviewKind(
  wsPath: string | WsFilePath,
): AssetPreviewKind | undefined {
  const filePath = WsPath.safeParseFile(wsPath).data;
  if (!filePath) {
    return undefined;
  }
  return assetPreviewKindForExtension(filePath.extension);
}

export function getEmbeddableWorkspaceAssetKind(
  wsPath: string | WsFilePath,
): EmbeddableWorkspaceAssetKind | undefined {
  const filePath = WsPath.safeParseFile(wsPath).data;
  if (!filePath) {
    return undefined;
  }

  if (isEmbeddableImageExtension(filePath.extension)) {
    return 'image';
  }

  return undefined;
}

export function isEmbeddableWorkspaceAsset(
  wsPath: string | WsFilePath,
): boolean {
  return getEmbeddableWorkspaceAssetKind(wsPath) !== undefined;
}

export function workspaceRootMarkdownAssetHref(
  assetWsPath: string | WsFilePath,
): string | undefined {
  const asset = WsPath.safeParse(assetWsPath).data?.asFile();
  if (!asset) {
    return undefined;
  }

  const encodedPath = asset.path
    .split('/')
    .filter(Boolean)
    .map(encodeSegment)
    .join('/');
  return encodedPath ? `/${encodedPath}` : undefined;
}

export function resolveWorkspaceMarkdownAssetReference(
  currentWsPath: string | WsFilePath,
  target: string,
): WsFilePath | undefined {
  return resolveWorkspaceMarkdownAssetReferenceCandidates(
    currentWsPath,
    target,
  )[0];
}

export function resolveWorkspaceMarkdownAssetReferenceCandidates(
  currentWsPath: string | WsFilePath,
  target: string,
): WsFilePath[] {
  const current = WsPath.safeParse(currentWsPath).data?.asFile();
  if (!current) {
    return [];
  }

  const input = target.trim();
  const direct = WsPath.safeParse(input).data?.asFile();
  if (direct) {
    return direct.wsName === current.wsName ? [direct] : [];
  }

  const candidates = [
    resolveLocalMarkdownAsset(current, input),
    input.startsWith('/')
      ? undefined
      : resolveLocalMarkdownAsset(current, `/${input}`),
  ].filter((candidate): candidate is WsFilePath => Boolean(candidate));

  return Array.from(
    new Map(candidates.map((candidate) => [candidate.wsPath, candidate])),
  ).map(([, candidate]) => candidate);
}

/**
 * Resolves a local Markdown asset target from a note without allowing workspace
 * escape or encoded path separators.
 */
export function resolveLocalMarkdownAsset(
  currentWsPath: string | WsFilePath,
  target: string,
): WsFilePath | undefined {
  const current = WsPath.safeParse(currentWsPath).data?.asFile();
  const input = target.trim();
  if (
    !current ||
    !input ||
    input.includes('\\') ||
    input.includes('?') ||
    input.includes('#') ||
    input.includes('//') ||
    EXPLICIT_SCHEME_RE.test(input)
  ) {
    return undefined;
  }

  const segments = input.startsWith('/')
    ? []
    : (current.getParent()?.path.split('/').filter(Boolean) ?? []);
  for (const encoded of input.replace(/^\//, '').split('/')) {
    if (!encoded || encoded === '.') {
      continue;
    }
    const segment = decodeSafeSegment(encoded);
    if (!segment) {
      return undefined;
    }
    if (segment === '.') {
      continue;
    }
    if (segment === '..') {
      if (!segments.pop()) {
        return undefined;
      }
      continue;
    }
    segments.push(segment);
  }

  return WsPath.safeFromParts(
    current.wsName,
    segments.join('/'),
  ).data?.asFile();
}

/** Creates a URL-style relative Markdown href from one note to a stored asset. */
export function relativeMarkdownAssetHref(
  sourceWsPath: string | WsFilePath,
  assetWsPath: string | WsFilePath,
): string | undefined {
  const source = WsPath.safeParse(sourceWsPath).data?.asFile();
  const asset = WsPath.safeParse(assetWsPath).data?.asFile();
  if (!source || !asset || source.wsName !== asset.wsName) {
    return undefined;
  }

  const sourceDir = source.getParent()?.path.split('/').filter(Boolean) ?? [];
  const assetParts = asset.path.split('/').filter(Boolean);
  const assetFileName = assetParts.at(-1);
  if (!assetFileName) {
    return undefined;
  }

  const assetDir = assetParts.slice(0, -1);
  let common = 0;
  while (
    common < sourceDir.length &&
    common < assetDir.length &&
    sourceDir[common] === assetDir[common]
  ) {
    common += 1;
  }

  const up = Array.from({ length: sourceDir.length - common }, () => '..');
  const down = assetDir.slice(common);
  const relative = [...up, ...down, assetFileName].map(encodeSegment);
  return relative.join('/') || encodeSegment(assetFileName);
}
