import { type RelatedQuery, resolveRelated } from './related.js';
import type { FileMeta } from './scan.js';

const BANNER = '<!-- AUTO-GENERATED by `zeno knowledge index` — do not edit by hand. -->';
const EMPTY_BODY =
  '_No knowledge files yet. Copy `_template.md` to start, then run `zeno knowledge index` to refresh this listing._';

export interface RenderResult {
  markdown: string;
  unresolvedRelated: RelatedQuery[];
}

export function renderIndex(files: FileMeta[], opts: { generatedAt: Date }): RenderResult {
  const totalBytes = files.reduce((acc, f) => acc + f.bytes, 0);
  const queries: RelatedQuery[] = [];
  for (const f of files) {
    for (const slug of f.related) {
      queries.push({ file: f.relPath, slug });
    }
  }
  const allPaths = files.map((f) => f.relPath);
  const resolution = resolveRelated(allPaths, queries);
  const unresolvedByFile = new Map<string, Set<string>>();
  for (const q of resolution.unresolved) {
    let set = unresolvedByFile.get(q.file);
    if (!set) {
      set = new Set();
      unresolvedByFile.set(q.file, set);
    }
    set.add(q.slug);
  }

  const lines: string[] = [];
  lines.push(BANNER, '');
  lines.push('# Knowledge Index', '');
  lines.push(`Last refreshed: ${opts.generatedAt.toISOString()}`);
  lines.push(`Total: ${files.length} files · ${formatBytes(totalBytes)}`);
  lines.push('');
  lines.push('## Files', '');

  if (files.length === 0) {
    lines.push(EMPTY_BODY, '');
  } else {
    lines.push(...renderTree(files, unresolvedByFile));
    lines.push('');
  }

  const tags = collectTags(files);
  if (tags.size > 0) {
    lines.push('## By tag', '');
    for (const tag of [...tags.keys()].sort()) {
      const filesForTag = tags.get(tag) ?? [];
      lines.push(`- \`${tag}\` — ${filesForTag.join(', ')}`);
    }
    lines.push('');
  }

  return { markdown: `${lines.join('\n').trimEnd()}\n`, unresolvedRelated: resolution.unresolved };
}

function renderTree(files: FileMeta[], unresolved: Map<string, Set<string>>): string[] {
  const lines: string[] = [];
  let lastDir: string[] = [];

  for (const f of files) {
    const parts = f.relPath.split('/');
    const fileName = parts.pop() ?? f.relPath;
    const dirParts = parts;

    if (!sameDir(dirParts, lastDir)) {
      const commonLen = commonPrefixLen(dirParts, lastDir);
      for (let i = commonLen; i < dirParts.length; i++) {
        const indent = '  '.repeat(i);
        lines.push(`${indent}- ${dirParts[i]}/`);
      }
      lastDir = dirParts;
    }

    const indent = '  '.repeat(dirParts.length);
    const desc = f.description.length > 0 ? ` — ${f.description}` : '';
    lines.push(`${indent}- [${fileName}](${f.relPath})${desc}`);
    if (f.related.length > 0) {
      const detail = f.related
        .map((slug) => {
          const set = unresolved.get(f.relPath);
          return set?.has(slug) ? `${slug} (⚠ unresolved)` : slug;
        })
        .join(', ');
      lines.push(`${indent}  related: ${detail}`);
    }
  }

  return lines;
}

function sameDir(a: string[], b: string[]): boolean {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}

function commonPrefixLen(a: string[], b: string[]): number {
  const n = Math.min(a.length, b.length);
  let i = 0;
  while (i < n && a[i] === b[i]) i++;
  return i;
}

function collectTags(files: FileMeta[]): Map<string, string[]> {
  const map = new Map<string, string[]>();
  for (const f of files) {
    for (const tag of f.tags) {
      const list = map.get(tag) ?? [];
      list.push(f.relPath);
      map.set(tag, list);
    }
  }
  return map;
}

function formatBytes(n: number): string {
  if (n < 1024) return `${n} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
