import { readdirSync, readFileSync, statSync } from "node:fs";
import { relative, resolve } from "node:path";
import YAML from "yaml";
import { envString } from "../env.js";
import { loadRagConfig } from "../rag/config.js";
import { getViaRag, isRagRemote, searchViaRag } from "../rag/client.js";

export type KnowledgeDoc = {
  id: string;
  title: string;
  source: string;
  tags: string[];
  owner?: string;
  updated?: string;
  headings: string[];
  content: string;
};

export type KnowledgeSearchResult = Omit<KnowledgeDoc, "content"> & {
  relevance: number;
  excerpt: string;
};

const defaultKnowledgeDir = () => resolve(process.cwd(), "../../knowledge");

const normalizePathId = (source: string) =>
  source.replace(/\.md$/i, "").split(/[\\/]/g).join(":");

const walkMarkdownFiles = (dir: string): string[] => {
  const entries = readdirSync(dir, { withFileTypes: true });

  return entries.flatMap((entry) => {
    const fullPath = resolve(dir, entry.name);

    if (entry.isDirectory()) {
      return walkMarkdownFiles(fullPath);
    }

    if (entry.isFile() && entry.name.endsWith(".md")) {
      return [fullPath];
    }

    return [];
  });
};

const parseFrontmatter = (raw: string) => {
  if (!raw.startsWith("---")) {
    return { metadata: {}, body: raw };
  }

  const end = raw.indexOf("\n---", 3);
  if (end === -1) {
    return { metadata: {}, body: raw };
  }

  const frontmatter = raw.slice(3, end).trim();
  const body = raw.slice(end + 4).trim();
  const metadata = YAML.parse(frontmatter) ?? {};

  return { metadata, body };
};

const extractHeadings = (body: string) =>
  body
    .split("\n")
    .filter((line) => line.startsWith("#"))
    .map((line) => line.replace(/^#+\s*/, "").trim())
    .filter(Boolean);

const tokenize = (value: string) =>
  value
    .toLowerCase()
    .split(/[^a-z0-9]+/i)
    .map((token) => token.trim())
    .filter((token) => token.length >= 2);

const scoreDoc = (doc: KnowledgeDoc, query: string) => {
  const tokens = tokenize(query);
  if (tokens.length === 0) {
    return 0.1;
  }

  const title = doc.title.toLowerCase();
  const tags = doc.tags.join(" ").toLowerCase();
  const headings = doc.headings.join(" ").toLowerCase();
  const source = doc.source.toLowerCase();
  const content = doc.content.toLowerCase();

  return tokens.reduce((score, token) => {
    if (title.includes(token)) score += 5;
    if (tags.includes(token)) score += 4;
    if (headings.includes(token)) score += 3;
    if (source.includes(token)) score += 2;
    if (content.includes(token)) score += 1;
    return score;
  }, 0);
};

const excerptFor = (content: string, query: string) => {
  const token = tokenize(query)[0];
  const compact = content.replace(/\s+/g, " ").trim();
  if (!token) return compact.slice(0, 220);

  const index = compact.toLowerCase().indexOf(token);
  if (index === -1) return compact.slice(0, 220);

  return compact.slice(Math.max(0, index - 80), index + 140);
};

// RAG-driven tag filter: a doc matches if it has at least one of the
// includeTags (if any) and none of the excludeTags.
const passesTagFilter = (
  doc: KnowledgeSearchResult,
  includeTags: string[],
  excludeTags: string[],
): boolean => {
  if (includeTags.length > 0) {
    const hasIncluded = doc.tags.some((tag) => includeTags.includes(tag));
    if (!hasIncluded) return false;
  }
  if (excludeTags.length > 0) {
    const hasExcluded = doc.tags.some((tag) => excludeTags.includes(tag));
    if (hasExcluded) return false;
  }
  return true;
};

export const loadKnowledgeDocs = (knowledgeDir = envString(process.env.KNOWLEDGE_DIR, defaultKnowledgeDir())): KnowledgeDoc[] => {
  const root = resolve(knowledgeDir);

  try {
    statSync(root);
  } catch {
    return [];
  }

  return walkMarkdownFiles(root).map((filePath) => {
    const source = relative(root, filePath);
    const raw = readFileSync(filePath, "utf8");
    const { metadata, body } = parseFrontmatter(raw);
    const data = metadata as Record<string, unknown>;
    const headings = extractHeadings(body);

    return {
      id: normalizePathId(source),
      title: typeof data.title === "string" ? data.title : headings[0] ?? source,
      source,
      tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
      owner: typeof data.owner === "string" ? data.owner : undefined,
      updated: typeof data.updated === "string" ? data.updated : undefined,
      headings,
      content: body,
    };
  });
};

export const createDocsRepository = () => {
  const rag = loadRagConfig();
  const useRemote = isRagRemote(rag);

  return {
    async search(query: string, limit?: number): Promise<KnowledgeSearchResult[]> {
      const effectiveLimit = Math.max(1, limit ?? rag.topK);

      if (useRemote) {
        try {
          return await searchViaRag(rag, query, effectiveLimit);
        } catch (error) {
          if (!rag.fallbackToLocal) throw error;
        }
      }

      // Local fallback: read from knowledge/ and apply token-overlap scoring.
      const fullDocs = loadKnowledgeDocs().filter((doc) =>
        passesTagFilter(
          {
            id: doc.id,
            title: doc.title,
            source: doc.source,
            tags: doc.tags,
            owner: doc.owner,
            updated: doc.updated,
            headings: doc.headings,
            relevance: 0,
            excerpt: "",
          },
          rag.includeTags,
          rag.excludeTags,
        ),
      );
      const scored = fullDocs
        .map((doc) => ({
          ...doc,
          relevance: scoreDoc(doc, query),
          excerpt: excerptFor(doc.content, query),
        }))
        .filter((doc) => doc.relevance >= rag.minRelevance && doc.relevance > 0);

      return scored
        .sort((a, b) => b.relevance - a.relevance || a.title.localeCompare(b.title))
        .slice(0, effectiveLimit)
        .map(({ content: _content, ...doc }) => doc);
    },

    async get(id: string): Promise<KnowledgeDoc | undefined> {
      if (useRemote) {
        try {
          return await getViaRag(rag, id);
        } catch (error) {
          if (!rag.fallbackToLocal) throw error;
        }
      }
      return loadKnowledgeDocs().find((doc) => doc.id === id);
    },

    async list(limit = 500): Promise<KnowledgeSearchResult[]> {
      const candidates = (await getAllMetadataLocal())
        .filter((doc) => passesTagFilter(doc, rag.includeTags, rag.excludeTags))
        .slice(0, limit);
      return candidates;
    },

    async count(): Promise<number> {
      const candidates = await getAllMetadataLocal();
      return candidates.filter((doc) =>
        passesTagFilter(doc, rag.includeTags, rag.excludeTags),
      ).length;
    },
  };
};

const getAllMetadataLocal = async (): Promise<KnowledgeSearchResult[]> => {
  const rag = loadRagConfig();
  const useRemote = isRagRemote(rag);
  if (useRemote) {
    try {
      return await searchViaRag(rag, "", 1000);
    } catch (error) {
      if (!rag.fallbackToLocal) throw error;
    }
  }
  const docs = loadKnowledgeDocs();
  return docs.map((doc) => ({
    id: doc.id,
    title: doc.title,
    source: doc.source,
    tags: doc.tags,
    owner: doc.owner,
    updated: doc.updated,
    headings: doc.headings,
    relevance: 0,
    excerpt: "",
  }));
};
