import express, { type Application, type Request, type Response, type RequestHandler } from 'express';
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, lstatSync, renameSync, rmSync } from 'fs';
import { join, relative } from 'path';
import { randomBytes } from 'crypto';
import type { SkillCatalog, SkillEntry } from '../engine/skills.js';
import { VALID_SKILL_NAME } from '../engine/skills.js';
import { scanSkillContent, scanSkillDirectory, maxSeverity } from '../engine/skills-scanner.js';
import matter from 'gray-matter';
import { logger } from '../logger.js';
import { handleInstallFromUrl } from './skills-git-install.js';

const MAX_CONTENT_SIZE = 64 * 1024; // 64 KB

export interface MountSkillsApiOptions {
  skillCatalog: SkillCatalog;
  requireAuth?: RequestHandler;
  requireAdmin?: RequestHandler;
  authActive?: boolean;
  auditLog?: (jobId: string | null, action: string, actor: string, detail: object) => Promise<void>;
}

type AuthedUser = { id?: string; role?: string };

function getUser(req: Request): AuthedUser | undefined {
  return (req as any).user as AuthedUser | undefined;
}

function getUserId(req: Request): string {
  const user = getUser(req);
  return user?.id ?? 'local';
}

function isAdmin(req: Request): boolean {
  const user = getUser(req);
  return user?.role === 'admin';
}

/**
 * Recursively list files in a directory, skipping symlinks.
 * Returns paths relative to `baseDir`.
 */
function listDirFiles(baseDir: string, maxDepth: number = 5): string[] {
  const results: string[] = [];

  function walk(dir: string, depth: number): void {
    if (depth > maxDepth) return;
    let entries: string[];
    try {
      entries = readdirSync(dir);
    } catch {
      return;
    }
    for (const entry of entries) {
      const fullPath = join(dir, entry);
      let stat;
      try {
        stat = lstatSync(fullPath);
      } catch {
        continue;
      }
      if (stat.isSymbolicLink()) continue;
      if (stat.isDirectory()) {
        walk(fullPath, depth + 1);
      } else if (stat.isFile()) {
        results.push(relative(baseDir, fullPath));
      }
    }
  }

  walk(baseDir, 0);
  return results;
}

export function mountSkillsApi(app: Application, opts: MountSkillsApiOptions): void {
  const { skillCatalog } = opts;

  // JSON body parser for skills endpoints
  app.use('/api/skills', express.json());

  // Auth gating
  if (opts.authActive && opts.requireAuth) {
    app.use('/api/skills', opts.requireAuth);
  }

  // ── GET /api/skills ── list skills ──────────────────────────────
  app.get('/api/skills', (req: Request, res: Response) => {
    try {
      const userId = getUserId(req);
      const scope = (req.query.scope as string) ?? 'all';

      if (!['all', 'system', 'user'].includes(scope)) {
        res.status(400).json({ error: 'scope must be one of: all, system, user' });
        return;
      }

      const entries = skillCatalog.getForUser(userId);
      const filtered = scope === 'all'
        ? entries
        : entries.filter(e => e.source === scope);

      const skills = filtered.map(e => ({
        name: e.name,
        description: e.description,
        triggers: e.triggers,
        source: e.source,
        hasDir: e.dirPath !== null,
      }));

      res.json({ skills });
    } catch (e) {
      res.status(500).json({ error: `Failed to list skills: ${e}` });
    }
  });

  // ── POST /api/skills/install-from-url ── Git URL install ────────
  // Must be before /:name routes to avoid Express matching 'install-from-url' as :name
  app.post('/api/skills/install-from-url', handleInstallFromUrl({
    skillCatalog: opts.skillCatalog,
    auditLog: opts.auditLog,
  }));

  // ── GET /api/skills/:name ── skill detail ──────────────────────
  app.get('/api/skills/:name', (req: Request, res: Response) => {
    const { name } = req.params;
    if (!VALID_SKILL_NAME.test(name)) {
      res.status(400).json({ error: 'Invalid skill name' });
      return;
    }

    try {
      const userId = getUserId(req);
      const scopeHint = req.query.scope as string | undefined;

      // Find the entry matching name (and optional scope filter)
      const entries = skillCatalog.getForUser(userId);
      let entry: SkillEntry | undefined;
      if (scopeHint && ['system', 'user'].includes(scopeHint)) {
        entry = entries.find(e => e.name === name && e.source === scopeHint);
      }
      if (!entry) {
        entry = entries.find(e => e.name === name);
      }
      if (!entry) {
        res.status(404).json({ error: 'Skill not found' });
        return;
      }

      // Read content via catalog
      const contentResult = skillCatalog.getSkillContent(name, userId);
      const content = contentResult?.content ?? '';

      // Read raw file for frontmatter
      let raw = '';
      try {
        raw = readFileSync(entry.filePath, 'utf-8');
      } catch { /* skip */ }

      // File listing for directory skills
      let files: string[] | undefined;
      if (entry.dirPath) {
        files = listDirFiles(entry.dirPath);
      }

      // Security scan
      let findings;
      if (entry.dirPath) {
        findings = scanSkillDirectory(entry.dirPath);
      } else {
        findings = scanSkillContent(raw);
      }

      res.json({
        name: entry.name,
        description: entry.description,
        triggers: entry.triggers,
        source: entry.source,
        hasDir: entry.dirPath !== null,
        content,
        // Full file incl. frontmatter — what the editor must load and save back.
        // Editing/saving the body-only `content` would drop the frontmatter and
        // make the skill unreadable (it vanishes from the catalog).
        raw,
        files,
        findings,
        maxSeverity: maxSeverity(findings),
      });
    } catch (e) {
      res.status(500).json({ error: `Failed to read skill: ${e}` });
    }
  });

  // ── POST /api/skills ── create single-file skill ──────────────
  app.post('/api/skills', async (req: Request, res: Response) => {
    try {
      const { name, content, scope } = req.body ?? {};

      // Validate name
      if (!name || typeof name !== 'string' || !VALID_SKILL_NAME.test(name)) {
        res.status(400).json({ error: 'Invalid skill name (lowercase alphanumeric, hyphens, underscores)' });
        return;
      }

      // Validate scope
      if (!scope || !['system', 'user'].includes(scope)) {
        res.status(400).json({ error: 'scope must be one of: system, user' });
        return;
      }

      // Validate content
      if (!content || typeof content !== 'string') {
        res.status(400).json({ error: 'content is required' });
        return;
      }

      if (Buffer.byteLength(content, 'utf-8') > MAX_CONTENT_SIZE) {
        res.status(400).json({ error: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB` });
        return;
      }

      // System scope requires admin
      if (scope === 'system' && !isAdmin(req)) {
        res.status(403).json({ error: 'Only admins can create system skills' });
        return;
      }

      const userId = getUserId(req);

      // Determine destination directory
      const destDir = scope === 'system'
        ? skillCatalog.getSystemDir()
        : skillCatalog.getUserSkillDir(userId);

      // Check for existing skill (directory or flat file)
      const destDirPath = join(destDir, name);
      const destFlatPath = join(destDir, `${name}.md`);
      if (existsSync(destDirPath) || existsSync(destFlatPath)) {
        res.status(409).json({ error: 'Skill already exists' });
        return;
      }

      // Scan content before writing
      const findings = scanSkillContent(content);
      const severity = maxSeverity(findings);

      // Always create directory format: {name}/SKILL.md
      const tmpDir = join(destDir, `.tmp-${randomBytes(8).toString('hex')}`);
      mkdirSync(tmpDir, { recursive: true });
      writeFileSync(join(tmpDir, 'SKILL.md'), content, 'utf-8');
      renameSync(tmpDir, destDirPath);

      // Invalidate cache
      if (scope === 'system') {
        skillCatalog.refreshSystem();
      } else {
        skillCatalog.invalidate(userId);
      }

      // Audit log
      const actor = userId;
      if (opts.auditLog) {
        await opts.auditLog(null, 'skill.create', actor, { name, scope, severity });
      }

      logger.info(`[skills-api] created skill=${name} scope=${scope} actor=${actor} severity=${severity}`);
      res.status(201).json({ name, scope, severity, findings });
    } catch (e) {
      res.status(500).json({ error: `Failed to create skill: ${e}` });
    }
  });

  // ── PUT /api/skills/:name ── edit skill content ───────────────
  app.put('/api/skills/:name', async (req: Request, res: Response) => {
    const { name } = req.params;
    if (!VALID_SKILL_NAME.test(name)) {
      res.status(400).json({ error: 'Invalid skill name' });
      return;
    }

    const scope = req.query.scope as string | undefined;
    if (!scope) {
      res.status(400).json({ error: 'scope query parameter is required' });
      return;
    }
    if (!['system', 'user'].includes(scope)) {
      res.status(400).json({ error: 'scope must be one of: system, user' });
      return;
    }

    // System scope requires admin
    if (scope === 'system' && !isAdmin(req)) {
      res.status(403).json({ error: 'Only admins can edit system skills' });
      return;
    }

    try {
      const { content } = req.body ?? {};
      if (!content || typeof content !== 'string') {
        res.status(400).json({ error: 'content is required' });
        return;
      }

      if (Buffer.byteLength(content, 'utf-8') > MAX_CONTENT_SIZE) {
        res.status(400).json({ error: `Content exceeds maximum size of ${MAX_CONTENT_SIZE / 1024}KB` });
        return;
      }

      const userId = getUserId(req);
      const baseDir = scope === 'system'
        ? skillCatalog.getSystemDir()
        : skillCatalog.getUserSkillDir(userId);

      // Find the skill file: either flat file or directory with SKILL.md
      let targetPath: string | null = null;
      const flatPath = join(baseDir, `${name}.md`);
      const dirSkillPath = join(baseDir, name, 'SKILL.md');

      if (existsSync(dirSkillPath)) {
        targetPath = dirSkillPath;
      } else if (existsSync(flatPath)) {
        targetPath = flatPath;
      }

      if (!targetPath) {
        res.status(404).json({ error: 'Skill not found' });
        return;
      }

      // Guard against destroying the skill: the catalog identifies a skill by
      // its frontmatter `name`. Saving content without a valid `name` (e.g. a
      // body-only edit that dropped the frontmatter) would make it unreadable
      // and vanish from the list. Require valid frontmatter matching the skill.
      let fmName = '';
      try {
        const parsed = matter(content);
        fmName = typeof parsed.data?.name === 'string' ? parsed.data.name : '';
      } catch {
        fmName = '';
      }
      if (!fmName || !VALID_SKILL_NAME.test(fmName)) {
        res.status(400).json({
          error: 'Content must begin with YAML frontmatter containing a valid "name" (otherwise the skill becomes unreadable). Edit the full SKILL.md, including the --- frontmatter --- block.',
        });
        return;
      }
      if (fmName !== name) {
        res.status(400).json({
          error: `Frontmatter name "${fmName}" must match the skill name "${name}". Renaming via edit is not supported.`,
        });
        return;
      }

      // Scan new content
      const findings = scanSkillContent(content);
      const severity = maxSeverity(findings);

      // Atomic write: tmpfile in same directory as target → rename
      const targetDir = targetPath === dirSkillPath ? join(baseDir, name) : baseDir;
      const tmpPath = join(targetDir, `.tmp-${randomBytes(8).toString('hex')}.md`);
      writeFileSync(tmpPath, content, 'utf-8');
      renameSync(tmpPath, targetPath);

      // Invalidate cache
      if (scope === 'system') {
        skillCatalog.refreshSystem();
      } else {
        skillCatalog.invalidate(userId);
      }

      // Audit log
      const actor = userId;
      if (opts.auditLog) {
        await opts.auditLog(null, 'skill.update', actor, { name, scope, severity });
      }

      logger.info(`[skills-api] updated skill=${name} scope=${scope} actor=${actor} severity=${severity}`);
      res.json({ ok: true, severity, findings });
    } catch (e) {
      res.status(500).json({ error: `Failed to update skill: ${e}` });
    }
  });

  // ── DELETE /api/skills/:name ── delete skill ──────────────────
  app.delete('/api/skills/:name', async (req: Request, res: Response) => {
    const { name } = req.params;
    if (!VALID_SKILL_NAME.test(name)) {
      res.status(400).json({ error: 'Invalid skill name' });
      return;
    }

    const scope = req.query.scope as string | undefined;
    if (!scope) {
      res.status(400).json({ error: 'scope query parameter is required' });
      return;
    }
    if (!['system', 'user'].includes(scope)) {
      res.status(400).json({ error: 'scope must be one of: system, user' });
      return;
    }

    // System scope requires admin
    if (scope === 'system' && !isAdmin(req)) {
      res.status(403).json({ error: 'Only admins can delete system skills' });
      return;
    }

    try {
      const userId = getUserId(req);
      const baseDir = scope === 'system'
        ? skillCatalog.getSystemDir()
        : skillCatalog.getUserSkillDir(userId);

      // Find the skill: directory or flat file
      const dirPath = join(baseDir, name);
      const flatPath = join(baseDir, `${name}.md`);
      let deleted = false;

      if (existsSync(dirPath) && lstatSync(dirPath).isDirectory()) {
        rmSync(dirPath, { recursive: true, force: true });
        deleted = true;
      } else if (existsSync(flatPath) && lstatSync(flatPath).isFile()) {
        unlinkSync(flatPath);
        deleted = true;
      }

      if (!deleted) {
        res.status(404).json({ error: 'Skill not found' });
        return;
      }

      // Invalidate cache
      if (scope === 'system') {
        skillCatalog.refreshSystem();
      } else {
        skillCatalog.invalidate(userId);
      }

      // Audit log
      const actor = userId;
      if (opts.auditLog) {
        await opts.auditLog(null, 'skill.delete', actor, { name, scope });
      }

      logger.info(`[skills-api] deleted skill=${name} scope=${scope} actor=${actor}`);
      res.json({ ok: true });
    } catch (e) {
      res.status(500).json({ error: `Failed to delete skill: ${e}` });
    }
  });
}
