import { promises as fs } from 'node:fs';
import { join, isAbsolute, basename } from 'node:path';
import { pathGuard, PathScopeError } from './path_guard.js';
import { stripQuotes } from '../utils/string-utils.js';

// Batch 9.6: read-only skill library. Folders under /data/skills/<group>/<skill>/
// contain a SKILL.md with YAML frontmatter (name + description) and a markdown
// body. Three tools expose the library: skill_find (search), skill_use (load
// body), skill_resource (read a support file inside the folder).
//
// Layout is intentionally uniform — scan /data/skills/*/*/SKILL.md at fixed
// depth 3. Group folders (depth 1) hold LICENSE + ATTRIBUTION.md + skill
// subfolders and are NOT themselves skills. Support files inside skill
// folders are reachable via skill_resource, never auto-parsed.
//
// Cache model mirrors agents.ts: walk on first access, TTL re-walk to pick up
// new skills, per-entry mtime check on body access so a hot-edited SKILL.md
// is re-read without a restart. No watcher.

const SKILLS_ROOT = process.env.SKILLS_ROOT ?? '/data/skills';
const MAX_RESOURCE_BYTES = 5 * 1024 * 1024;
const LIST_CACHE_TTL_MS = 60_000;

export interface Skill {
  name: string;
  description: string;
  path: string;
  mtime: number;
}

interface CachedSkill extends Skill {
  body: string;
}

const cache = new Map<string, CachedSkill>();
let lastWalkedAt = 0;

interface Frontmatter {
  name?: string;
  description?: string;
}

function parseFrontmatter(yaml: string): Frontmatter {
  const fm: Frontmatter = {};
  for (const raw of yaml.split('\n')) {
    const line = raw.trim();
    if (line.length === 0) continue;
    const colon = line.indexOf(':');
    if (colon < 0) continue;
    const key = line.slice(0, colon).trim();
    const val = stripQuotes(line.slice(colon + 1).trim());
    if (key === 'name') fm.name = val;
    else if (key === 'description') fm.description = val;
  }
  return fm;
}

interface ParsedSkillFile {
  name: string;
  description: string;
  body: string;
}

function parseSkillFile(content: string): ParsedSkillFile {
  const lines = content.split('\n');
  let openIdx = -1;
  for (let i = 0; i < lines.length; i++) {
    const t = lines[i]!.trim();
    if (t === '') continue;
    if (t === '---') openIdx = i;
    break;
  }
  if (openIdx < 0) throw new Error('missing opening --- fence');
  let closeIdx = -1;
  for (let i = openIdx + 1; i < lines.length; i++) {
    if (lines[i]!.trim() === '---') { closeIdx = i; break; }
  }
  if (closeIdx < 0) throw new Error('missing closing --- fence');

  const yamlText = lines.slice(openIdx + 1, closeIdx).join('\n');
  const body = lines.slice(closeIdx + 1).join('\n');

  const fm = parseFrontmatter(yamlText);
  if (!fm.name) throw new Error('frontmatter missing name');
  if (!fm.description) throw new Error('frontmatter missing description');
  return { name: fm.name, description: fm.description, body };
}

// Fixed depth-3 scan: /data/skills/<group>/<skill>/SKILL.md. Two layers of
// readdir, no recursion. Group folders without SKILL.md are skipped silently;
// LICENSE / ATTRIBUTION.md / other non-SKILL.md files are ignored entirely.
// Returns all parseable skills as-found — dedup + collision logging happens
// in ensureCache where the sort order is established.
async function walkSkills(root: string): Promise<CachedSkill[]> {
  const found: CachedSkill[] = [];
  let groups;
  try {
    groups = await fs.readdir(root, { withFileTypes: true });
  } catch {
    return found;
  }
  for (const group of groups) {
    if (!group.isDirectory() || group.name.startsWith('.')) continue;
    const groupPath = join(root, group.name);
    let entries;
    try {
      entries = await fs.readdir(groupPath, { withFileTypes: true });
    } catch {
      continue;
    }
    for (const entry of entries) {
      if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
      const skillFolder = join(groupPath, entry.name);
      const skillFile = join(skillFolder, 'SKILL.md');
      let stat;
      try {
        stat = await fs.stat(skillFile);
      } catch {
        continue; // folder without SKILL.md — silent skip
      }
      if (!stat.isFile()) continue;
      try {
        const content = await fs.readFile(skillFile, 'utf8');
        const parsed = parseSkillFile(content);
        found.push({
          name: parsed.name,
          description: parsed.description,
          path: skillFolder,
          mtime: stat.mtimeMs,
          body: parsed.body,
        });
      } catch (err) {
        const reason = err instanceof Error ? err.message : String(err);
        console.warn(`skills: failed to parse ${skillFile} — ${reason}`);
      }
    }
  }
  return found;
}

async function ensureCache(): Promise<void> {
  const now = Date.now();
  if (cache.size > 0 && now - lastWalkedAt < LIST_CACHE_TTL_MS) return;
  let stat;
  try {
    stat = await fs.stat(SKILLS_ROOT);
  } catch {
    cache.clear();
    lastWalkedAt = now;
    return;
  }
  if (!stat.isDirectory()) {
    cache.clear();
    lastWalkedAt = now;
    return;
  }
  const found = await walkSkills(SKILLS_ROOT);
  // Sort by name asc, then path asc — gives alphabetically-first-wins on
  // collision and stable, deterministic ordering for /api/skills + skill_find.
  found.sort((a, b) => {
    const n = a.name.localeCompare(b.name);
    return n !== 0 ? n : a.path.localeCompare(b.path);
  });
  cache.clear();
  const winnerPath = new Map<string, string>();
  for (const skill of found) {
    const prev = winnerPath.get(skill.name);
    if (prev) {
      console.warn(
        `skills: name collision "${skill.name}" — kept ${prev}, skipped ${skill.path}`,
      );
      continue;
    }
    winnerPath.set(skill.name, skill.path);
    cache.set(skill.name, skill);
  }
  lastWalkedAt = now;
}

export async function listSkills(): Promise<Skill[]> {
  await ensureCache();
  return Array.from(cache.values()).map((s) => ({
    name: s.name,
    description: s.description,
    path: s.path,
    mtime: s.mtime,
  }));
}

export interface SkillSummary {
  name: string;
  description: string;
}

export async function findSkills(query: string): Promise<SkillSummary[]> {
  await ensureCache();
  const all = Array.from(cache.values());
  const q = (query ?? '').trim().toLowerCase();
  if (q === '' || q === '*') {
    return all.map((s) => ({ name: s.name, description: s.description }));
  }
  // name match weighted 2x description match. No fancy ranking — substring
  // scoring is enough for ≤20 skills.
  const scored = all
    .map((s) => {
      let score = 0;
      if (s.name.toLowerCase().includes(q)) score += 2;
      if (s.description.toLowerCase().includes(q)) score += 1;
      return { s, score };
    })
    .filter((x) => x.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, 5);
  return scored.map(({ s }) => ({ name: s.name, description: s.description }));
}

// Returns the SKILL.md body with frontmatter stripped, or null if the skill
// is unknown. Single-entry mtime refresh: a hot edit shows up on next call.
export async function getSkillBody(name: string): Promise<string | null> {
  await ensureCache();
  const cached = cache.get(name);
  if (!cached) return null;

  let stat;
  try {
    stat = await fs.stat(join(cached.path, 'SKILL.md'));
  } catch {
    cache.delete(name);
    return null;
  }
  if (stat.mtimeMs === cached.mtime) return cached.body;
  try {
    const raw = await fs.readFile(join(cached.path, 'SKILL.md'), 'utf8');
    const parsed = parseSkillFile(raw);
    if (parsed.name !== name) {
      // Skill renamed itself; drop the stale entry. Next listSkills() walks.
      cache.delete(name);
      return null;
    }
    cached.body = parsed.body;
    cached.description = parsed.description;
    cached.mtime = stat.mtimeMs;
    return cached.body;
  } catch (err) {
    const reason = err instanceof Error ? err.message : String(err);
    console.warn(`skills: re-parse failed for ${name} — ${reason}`);
    cache.delete(name);
    return null;
  }
}

export type SkillResourceErrorCode = 'unknown_skill' | 'unknown_resource' | 'path_escape';

export type SkillResourceResult =
  | { ok: true; content: string }
  | { ok: false; code: SkillResourceErrorCode; message: string };

export async function getSkillResource(
  name: string,
  relativePath: string,
): Promise<SkillResourceResult> {
  await ensureCache();
  const cached = cache.get(name);
  if (!cached) {
    return { ok: false, code: 'unknown_skill', message: `unknown skill: ${name}` };
  }
  if (typeof relativePath !== 'string' || relativePath.trim() === '') {
    return { ok: false, code: 'unknown_resource', message: 'path is required' };
  }
  // Syntactic pre-check — catches the common "../../etc/passwd" attempt
  // before realpath dereferences any symlinks.
  if (isAbsolute(relativePath) || relativePath.split(/[\\/]/).some((seg) => seg === '..')) {
    return { ok: false, code: 'path_escape', message: `path escapes skill folder: ${relativePath}` };
  }
  // SKILL.md is the manifest — skill_use is the right tool to read it.
  if (basename(relativePath) === 'SKILL.md') {
    return { ok: false, code: 'unknown_resource', message: 'use skill_use to read SKILL.md' };
  }
  let real: string;
  try {
    real = await pathGuard(cached.path, relativePath);
  } catch (err) {
    if (err instanceof PathScopeError) {
      const code: SkillResourceErrorCode = err.message.includes('escapes')
        ? 'path_escape'
        : 'unknown_resource';
      return { ok: false, code, message: err.message };
    }
    throw err;
  }
  const stat = await fs.stat(real);
  if (!stat.isFile()) {
    return { ok: false, code: 'unknown_resource', message: 'not a file' };
  }
  if (stat.size > MAX_RESOURCE_BYTES) {
    return {
      ok: false,
      code: 'unknown_resource',
      message: `file too large (${stat.size} bytes, max ${MAX_RESOURCE_BYTES})`,
    };
  }
  const content = await fs.readFile(real, 'utf8');
  return { ok: true, content };
}
