import { mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
import path from "node:path";

import { webExtract, webSearch } from "./geminiWebResearchTools.ts";

type FetchLike = typeof fetch;

const SKILL_ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const MAX_LOCAL_RESULTS = 30;

async function walkSkillFiles(projectRoot: string): Promise<string[]> {
  const root = path.join(projectRoot, "skills");
  const out: string[] = [];
  const queue = [root];
  while (queue.length && out.length < 1_000) {
    const dir = queue.pop()!;
    let entries;
    try {
      entries = await readdir(dir, { withFileTypes: true });
    } catch {
      continue;
    }
    for (const entry of entries) {
      const abs = path.join(dir, entry.name);
      if (entry.isDirectory()) queue.push(abs);
      else if (entry.isFile() && entry.name === "SKILL.md") out.push(abs);
    }
  }
  return out;
}

function skillFrontmatter(raw: string): { name: string; description: string } {
  const match = raw.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
  const fm = match?.[1] ?? "";
  return {
    name: fm.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? "",
    description: fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "",
  };
}

async function localSkillSearch(projectRoot: string, query: string): Promise<Array<Record<string, unknown>>> {
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
  const rows: Array<Record<string, unknown> & { score: number }> = [];
  for (const abs of await walkSkillFiles(projectRoot)) {
    const raw = await readFile(abs, "utf8");
    const fm = skillFrontmatter(raw);
    const haystack = `${fm.name} ${fm.description} ${raw.slice(0, 8_000)}`.toLowerCase();
    const score = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
    if (score === 0) continue;
    rows.push({
      provider: "local",
      skill_id: fm.name || path.basename(path.dirname(abs)),
      title: fm.name || path.basename(path.dirname(abs)),
      description: fm.description,
      skill_path: path.relative(projectRoot, abs).replace(/\\/g, "/"),
      score,
    });
  }
  return rows.sort((a, b) => b.score - a.score).slice(0, MAX_LOCAL_RESULTS);
}

async function githubRepositorySearch(query: string, limit: number, fetchImpl: FetchLike): Promise<Array<Record<string, unknown>>> {
  const headers: Record<string, string> = {
    accept: "application/vnd.github+json",
    "user-agent": "CodeFlowMu-SkillDiscovery/1.0",
    "x-github-api-version": "2022-11-28",
  };
  const token = process.env.GITHUB_TOKEN?.trim();
  if (token) headers.authorization = `Bearer ${token}`;
  let items: Array<Record<string, unknown>> = [];
  for (const githubQuery of [query, `${query} agent`, `${query} MCP`]) {
    const url = new URL("https://api.github.com/search/repositories");
    url.searchParams.set("q", githubQuery);
    url.searchParams.set("sort", "stars");
    url.searchParams.set("per_page", String(Math.min(10, limit)));
    const response = await fetchImpl(url, { headers });
    if (!response.ok) throw new Error(`GitHub search failed: HTTP ${response.status}`);
    const json = await response.json() as { items?: Array<Record<string, unknown>> };
    items = json.items ?? [];
    if (items.length) break;
  }
  return items.slice(0, limit).map((item) => ({
    provider: "github",
    title: item.full_name,
    description: item.description ?? "",
    source_url: item.html_url,
    stars: item.stargazers_count,
    updated_at: item.updated_at,
    license: (item.license as Record<string, unknown> | null)?.spdx_id ?? null,
  }));
}

export async function skillSearch(
  projectRoot: string,
  args: Record<string, unknown>,
  fetchImpl: FetchLike = fetch,
): Promise<Record<string, unknown>> {
  const query = String(args.query ?? "").trim();
  if (!query) throw new Error("skill_search requires query");
  const providers = Array.isArray(args.providers)
    ? args.providers.map(String).map((p) => p.toLowerCase())
    : ["local", "github", "web"];
  const limit = Math.min(10, Math.max(1, Number(args.limit) || 5));
  const results: Array<Record<string, unknown>> = [];
  const errors: Array<{ provider: string; error: string }> = [];
  if (providers.includes("local")) results.push(...await localSkillSearch(projectRoot, query));
  if (providers.includes("github")) {
    try {
      results.push(...await githubRepositorySearch(query, limit, fetchImpl));
    } catch (error) {
      errors.push({ provider: "github", error: error instanceof Error ? error.message : String(error) });
    }
  }
  if (providers.includes("web")) {
    try {
      const web = await webSearch({ query: `${query} agent skill MCP tutorial documentation`, limit }, fetchImpl);
      results.push(...((web.results as Array<Record<string, unknown>>) ?? []).map((row) => ({ provider: "web", ...row })));
    } catch (error) {
      errors.push({ provider: "web", error: error instanceof Error ? error.message : String(error) });
    }
  }
  const provider_counts = Object.fromEntries(
    providers.map((provider) => [provider, results.filter((row) => row.provider === provider).length]),
  );
  return { query, providers, searched_at: new Date().toISOString(), provider_counts, results, errors };
}

function githubRawUrl(value: string): string {
  const match = value.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
  return match
    ? `https://raw.githubusercontent.com/${match[1]}/${match[2]}/${match[3]}/${match[4]}`
    : value;
}

export async function skillLearn(
  projectRoot: string,
  args: Record<string, unknown>,
  fetchImpl: FetchLike = fetch,
): Promise<Record<string, unknown>> {
  const capability = String(args.capability ?? "").trim();
  if (!capability) throw new Error("skill_learn requires capability");
  const sourceUrls = Array.isArray(args.source_urls) ? args.source_urls.map(String).filter(Boolean).slice(0, 8) : [];
  const localIds = Array.isArray(args.local_skill_ids) ? args.local_skill_ids.map(String).filter(Boolean).slice(0, 8) : [];
  const sources: Array<Record<string, unknown>> = [];
  for (const id of localIds) {
    if (!SKILL_ID_RE.test(id)) continue;
    const file = path.join(projectRoot, "skills", id, "SKILL.md");
    try {
      const raw = await readFile(file, "utf8");
      sources.push({ source_type: "local_skill", source_url: `skills/${id}/SKILL.md`, title: skillFrontmatter(raw).name || id, content: raw.slice(0, 30_000) });
    } catch (error) {
      sources.push({ source_type: "local_skill", source_url: `skills/${id}/SKILL.md`, error: error instanceof Error ? error.message : String(error) });
    }
  }
  for (const sourceUrl of sourceUrls) {
    try {
      const extracted = await webExtract({ url: githubRawUrl(sourceUrl), min_content_chars: 20 }, fetchImpl, projectRoot);
      sources.push({ source_type: "web", source_url: sourceUrl, final_url: extracted.final_url, title: extracted.title, content: extracted.content.slice(0, 30_000), tables: extracted.tables.slice(0, 5) });
    } catch (error) {
      sources.push({ source_type: "web", source_url: sourceUrl, error: error instanceof Error ? error.message : String(error) });
    }
  }
  return {
    capability,
    learned_at: new Date().toISOString(),
    sources,
    learning_contract: {
      required_output: ["capability_summary", "dependencies", "workflow", "risks", "minimal_validation", "reuse_decision"],
      rule: "Reading is not validation. Run the proposed minimal_validation before calling skill_publish.",
    },
  };
}

async function writeJsonAtomic(file: string, value: unknown): Promise<void> {
  await mkdir(path.dirname(file), { recursive: true });
  const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
  await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
  await rename(temp, file);
}

async function updateManifest(file: string, entry: Record<string, unknown>): Promise<void> {
  const raw = await readFile(file, "utf8");
  const manifest = JSON.parse(raw) as Record<string, unknown>;
  const list = Array.isArray(manifest.common_skills) ? manifest.common_skills as Array<Record<string, unknown>> : [];
  const index = list.findIndex((item) => item.id === entry.id);
  if (index >= 0) list[index] = { ...list[index], ...entry };
  else list.push(entry);
  manifest.common_skills = list;
  await writeJsonAtomic(file, manifest);
}

export async function skillPublish(projectRoot: string, args: Record<string, unknown>): Promise<Record<string, unknown>> {
  const skillId = String(args.skill_id ?? "").trim();
  const displayName = String(args.display_name ?? skillId).trim();
  const description = String(args.description ?? "").trim();
  const body = String(args.body_markdown ?? "").trim();
  const evidence = Array.isArray(args.validation_evidence) ? args.validation_evidence.map(String).filter(Boolean) : [];
  const sources = Array.isArray(args.source_urls) ? args.source_urls.map(String).filter(Boolean) : [];
  if (!SKILL_ID_RE.test(skillId)) throw new Error("skill_id must be lowercase kebab-case");
  if (/\r|\n/.test(displayName) || /\r|\n/.test(description)) throw new Error("display_name and description must be single-line values");
  if (description.length < 20) throw new Error("skill_publish requires a specific description of at least 20 characters");
  if (body.length < 100) throw new Error("skill_publish requires body_markdown of at least 100 characters");
  if (args.validated !== true || evidence.length === 0) throw new Error("skill_publish requires validated=true and validation_evidence");
  if (sources.length === 0) throw new Error("skill_publish requires at least one source_url");
  const skillDir = path.join(projectRoot, "skills", skillId);
  const skillFile = path.join(skillDir, "SKILL.md");
  try {
    const existing = await stat(skillFile);
    if (existing.isFile() && args.update_existing !== true) throw new Error(`Skill already exists: ${skillId}; set update_existing=true to replace it`);
  } catch (error) {
    if (error instanceof Error && error.message.startsWith("Skill already exists")) throw error;
  }
  const content = `---\nname: ${skillId}\ndescription: ${description}\n---\n\n# ${displayName}\n\n${body}\n\n## Validation Evidence\n\n${evidence.map((item) => `- ${item}`).join("\n")}\n\n## Sources\n\n${sources.map((item) => `- ${item}`).join("\n")}\n`;
  await mkdir(skillDir, { recursive: true });
  await writeFile(skillFile, content, "utf8");
  const entry = {
    id: skillId,
    display_name: displayName,
    description,
    skill_package: `skills/${skillId}/SKILL.md`,
    status: "playbook_ready",
    source_urls: sources,
  };
  await updateManifest(path.join(projectRoot, "docs", "skills", "agent-skills.manifest.json"), entry);
  await updateManifest(path.join(projectRoot, ".codeflowmu", "agent-skills.manifest.json"), entry);
  return { published: true, skill_id: skillId, skill_package: entry.skill_package, manifests_updated: ["docs/skills/agent-skills.manifest.json", ".codeflowmu/agent-skills.manifest.json"], validation_evidence: evidence, source_urls: sources };
}

export async function invokeGeminiSkillEvolutionTool(projectRoot: string, toolName: "skill_search" | "skill_learn" | "skill_publish", args: Record<string, unknown>): Promise<string> {
  const result = toolName === "skill_search"
    ? await skillSearch(projectRoot, args)
    : toolName === "skill_learn"
      ? await skillLearn(projectRoot, args)
      : await skillPublish(projectRoot, args);
  return JSON.stringify(result, null, 2);
}
