/**
 * POST /api/skills/install-local
 *
 * Install a Forge skill from a user-supplied bundle. Two flavours:
 *
 *   multipart/form-data:
 *     file: .md      → single-file skill, written as SKILL.md
 *     file: .zip     → bundle. Must contain SKILL.md (either at the
 *                      root or inside a single wrapping subdir).
 *                      Any sibling files (scripts/, references/,
 *                      assets/) are copied verbatim.
 *     name           → optional, overrides the frontmatter / wrapper-dir
 *     project_path   → optional, install into the project's .claude/skills/
 *                      instead of the global ~/.claude/skills/
 *
 *   application/json:
 *     { name, content }            → quick single-file path for the
 *     { name, files: { path: content, ... } }   AI / scripts
 *
 * The DB row is upserted with `source: 'local'` so the sync layer
 * won't try to redownload it from the remote registry (and won't mark
 * it deleted when it's not in registry.json).
 */

import { NextResponse } from 'next/server';
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { join, dirname, normalize, sep } from 'node:path';
import AdmZip from 'adm-zip';
import { homedir } from 'node:os';
import { getDb } from '@/src/core/db/database';
import { getDbPath } from '@/src/config';

/**
 * Refuse to clobber an existing skill (registry-synced or previously
 * uploaded). The wrapper-dir / frontmatter inside a zip can easily
 * collide with an existing name without the user noticing — better to
 * fail loudly than silently overwrite. To re-install, the user must
 * uninstall the existing one first.
 */
function nameConflict(name: string): { conflict: true; source: 'registry' | 'local' } | { conflict: false } {
  const row = db().prepare('SELECT source FROM skills WHERE name = ?').get(name) as any;
  if (!row) return { conflict: false };
  return { conflict: true, source: row.source === 'local' ? 'local' : 'registry' };
}

const MAX_BYTES = 5 * 1024 * 1024;          // 5 MB
const MAX_FILE_BYTES = 1 * 1024 * 1024;     // 1 MB per file
const MAX_FILES = 50;

function db() { return getDb(getDbPath()); }

// ─── Helpers ──────────────────────────────────────────────

function getClaudeHome(): string {
  return process.env.CLAUDE_HOME || join(homedir(), '.claude');
}

function isValidName(s: string): boolean {
  return /^[a-z0-9][a-z0-9_-]*$/.test(s);
}

function safeEntryPath(name: string): string | null {
  if (!name || name.startsWith('/') || name.includes('..')) return null;
  const normalized = normalize(name).replace(/^[/\\]+/, '');
  if (normalized.split(sep).some((seg) => seg === '..' || seg === '')) return null;
  return normalized;
}

interface ParsedFrontmatter {
  name?: string;
  description?: string;
  version?: string;
}

/** Extract `name:` / `description:` / `version:` from a SKILL.md YAML frontmatter block. */
function parseSkillMd(md: string): ParsedFrontmatter {
  const fm = md.match(/^---\n([\s\S]*?)\n---/);
  if (!fm) return {};
  const out: ParsedFrontmatter = {};
  for (const line of fm[1].split('\n')) {
    const m = line.match(/^([a-zA-Z_]+):\s*(.*?)\s*$/);
    if (!m) continue;
    const key = m[1].toLowerCase();
    let val = m[2];
    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
      val = val.slice(1, -1);
    }
    if (key === 'name') out.name = val;
    else if (key === 'description') out.description = val;
    else if (key === 'version') out.version = val;
  }
  return out;
}

function ensureDir(p: string): void {
  if (!existsSync(p)) mkdirSync(p, { recursive: true, mode: 0o700 });
}

interface InstallPayload {
  name: string;
  files: Array<{ path: string; data: Buffer }>;
  description?: string;
  version?: string;
}

/**
 * Pre-validate that the bundle has a SKILL.md and a usable name.
 * Returns either a usable payload or an error.
 */
function buildPayload(
  files: Array<{ path: string; data: Buffer }>,
  nameOverride?: string,
): { ok: true; payload: InstallPayload } | { ok: false; error: string } {
  // Detect wrapper-dir layout: <name>/SKILL.md
  const skillMd = files.find((f) => f.path === 'SKILL.md' || f.path.toLowerCase() === 'skill.md');
  let nestedRoot = '';
  let actualSkill = skillMd;
  if (!skillMd) {
    // Look for <something>/SKILL.md as the only entry of its kind
    const candidates = files.filter((f) => /(^|\/)SKILL\.md$/i.test(f.path));
    if (candidates.length !== 1) {
      return { ok: false, error: 'zip must contain exactly one SKILL.md (at root or inside one wrapper directory)' };
    }
    actualSkill = candidates[0];
    nestedRoot = actualSkill.path.slice(0, -('SKILL.md'.length + 1));
  }
  const md = actualSkill!.data.toString('utf-8');
  const fm = parseSkillMd(md);
  const inferredName =
    nameOverride ||
    fm.name ||
    (nestedRoot && nestedRoot.split('/')[0]) ||
    '';
  if (!inferredName) {
    return { ok: false, error: 'cannot determine skill name — set frontmatter `name:` or wrap files in a <name>/ directory' };
  }
  if (!isValidName(inferredName)) {
    return { ok: false, error: `skill name "${inferredName}" must be lowercase alphanumerics + hyphens/underscores` };
  }

  // Strip the wrapper-dir prefix from all file paths
  const flatFiles = files
    .map((f) => ({
      path: nestedRoot && f.path.startsWith(nestedRoot) ? f.path.slice(nestedRoot.length) : f.path,
      data: f.data,
    }))
    .filter((f) => f.path && f.path !== '');

  return {
    ok: true,
    payload: { name: inferredName, files: flatFiles, description: fm.description, version: fm.version },
  };
}

function writeSkillToDisk(payload: InstallPayload, projectPath?: string): { targetDir: string } {
  const root = projectPath
    ? join(projectPath, '.claude', 'skills', payload.name)
    : join(getClaudeHome(), 'skills', payload.name);
  // Clean any prior install so removed files don't linger.
  if (existsSync(root)) {
    try { rmSync(root, { recursive: true, force: true }); } catch {}
  }
  ensureDir(root);
  for (const f of payload.files) {
    const target = join(root, f.path);
    ensureDir(dirname(target));
    writeFileSync(target, f.data, { mode: 0o600 });
  }
  // Synthesize info.json if the upload didn't include one. The sync /
  // refresh layer reads version + description from here; without it,
  // a hand-uploaded skill shows up with blank metadata. `source: "local"`
  // (plus uploaded_at) marks it as a manual upload — distinguishable
  // from anything pulled from the remote registry.
  const hasInfoJson = payload.files.some((f) => f.path === 'info.json');
  if (!hasInfoJson) {
    const info = {
      name: payload.name,
      version: payload.version || '0.1.0',
      description: payload.description || '',
      tags: [],
      score: 0,
      rating: 0,
      source: 'local',
      uploaded_at: new Date().toISOString(),
    };
    writeFileSync(join(root, 'info.json'), JSON.stringify(info, null, 2), { mode: 0o600 });
  }
  return { targetDir: root };
}

function upsertSkillRow(payload: InstallPayload, projectPath?: string): void {
  const existing = db().prepare('SELECT installed_global, installed_projects, source FROM skills WHERE name = ?')
    .get(payload.name) as any;
  const installedProjects: string[] = existing
    ? (() => { try { return JSON.parse(existing.installed_projects || '[]'); } catch { return []; } })()
    : [];
  if (projectPath && !installedProjects.includes(projectPath)) installedProjects.push(projectPath);
  const installedGlobal = projectPath ? (existing?.installed_global ? 1 : 0) : 1;
  const version = payload.version || '0.1.0';

  db().prepare(`
    INSERT INTO skills (name, type, display_name, description, author, version, tags, score, rating,
                        source_url, archive, synced_at, source,
                        installed_global, installed_projects, installed_version)
    VALUES (?, 'skill', ?, ?, '', ?, '[]', 0, 0, '', '', datetime('now'), 'local', ?, ?, ?)
    ON CONFLICT(name) DO UPDATE SET
      display_name       = excluded.display_name,
      description        = excluded.description,
      version            = excluded.version,
      synced_at          = datetime('now'),
      source             = 'local',
      installed_global   = excluded.installed_global,
      installed_projects = excluded.installed_projects,
      installed_version  = excluded.installed_version
  `).run(
    payload.name,
    payload.name,
    payload.description || '',
    version,
    installedGlobal,
    JSON.stringify(installedProjects),
    version,
  );
}

// ─── POST ────────────────────────────────────────────────

export async function POST(req: Request) {
  const ct = req.headers.get('content-type') || '';

  // ── JSON path: { name, content } or { name, files: { path: content } }
  if (ct.includes('application/json')) {
    let body: any = {};
    try { body = await req.json(); } catch {}
    const nameRaw = String(body?.name || '').trim();
    if (body?.content) {
      // single SKILL.md
      const built = buildPayload(
        [{ path: 'SKILL.md', data: Buffer.from(String(body.content), 'utf-8') }],
        nameRaw || undefined,
      );
      if (!built.ok) return NextResponse.json({ ok: false, error: built.error }, { status: 400 });
      {
        const c = nameConflict(built.payload.name);
        if (c.conflict) {
          const which = c.source === 'registry' ? 'synced from the marketplace registry' : 'already uploaded';
          return NextResponse.json({ ok: false, error: `A skill named "${built.payload.name}" is ${which}. Uninstall it first, or rename your upload (or the SKILL.md frontmatter \`name:\`) before re-uploading.` }, { status: 409 });
        }
      }
      writeSkillToDisk(built.payload, body.project_path || undefined);
      upsertSkillRow(built.payload, body.project_path || undefined);
      return NextResponse.json({ ok: true, name: built.payload.name, version: built.payload.version || '0.1.0' });
    }
    if (body?.files && typeof body.files === 'object') {
      const entries = Object.entries(body.files as Record<string, string>);
      const built = buildPayload(
        entries.map(([path, content]) => ({ path, data: Buffer.from(String(content), 'utf-8') })),
        nameRaw || undefined,
      );
      if (!built.ok) return NextResponse.json({ ok: false, error: built.error }, { status: 400 });
      {
        const c = nameConflict(built.payload.name);
        if (c.conflict) {
          const which = c.source === 'registry' ? 'synced from the marketplace registry' : 'already uploaded';
          return NextResponse.json({ ok: false, error: `A skill named "${built.payload.name}" is ${which}. Uninstall it first, or rename your upload (or the SKILL.md frontmatter \`name:\`) before re-uploading.` }, { status: 409 });
        }
      }
      writeSkillToDisk(built.payload, body.project_path || undefined);
      upsertSkillRow(built.payload, body.project_path || undefined);
      return NextResponse.json({ ok: true, name: built.payload.name, version: built.payload.version || '0.1.0' });
    }
    return NextResponse.json({ ok: false, error: 'JSON body must have { content } or { files }' }, { status: 400 });
  }

  // ── multipart: file = .md | .zip
  if (ct.includes('multipart/form-data')) {
    let form: FormData;
    try { form = await req.formData(); }
    catch { return NextResponse.json({ ok: false, error: 'invalid multipart body' }, { status: 400 }); }
    const file = form.get('file');
    if (!(file instanceof File)) {
      return NextResponse.json({ ok: false, error: 'file field missing' }, { status: 400 });
    }
    if (file.size > MAX_BYTES) {
      return NextResponse.json({ ok: false, error: `file too large (max ${MAX_BYTES} bytes)` }, { status: 413 });
    }
    const nameOverride = String(form.get('name') || '').trim() || undefined;
    const projectPath = String(form.get('project_path') || '').trim() || undefined;
    const filename = file.name.toLowerCase();
    const buf = Buffer.from(await file.arrayBuffer());

    let files: Array<{ path: string; data: Buffer }> = [];

    if (filename.endsWith('.md')) {
      files.push({ path: 'SKILL.md', data: buf });
    } else if (filename.endsWith('.zip')) {
      let zip: AdmZip;
      try { zip = new AdmZip(buf); }
      catch (e) { return NextResponse.json({ ok: false, error: `invalid zip: ${(e as Error).message}` }, { status: 400 }); }
      const entries = zip.getEntries().filter((e) => !e.isDirectory);
      if (entries.length === 0) return NextResponse.json({ ok: false, error: 'zip is empty' }, { status: 400 });
      if (entries.length > MAX_FILES) return NextResponse.json({ ok: false, error: `zip has too many files (max ${MAX_FILES})` }, { status: 400 });
      for (const e of entries) {
        const safe = safeEntryPath(e.entryName);
        if (!safe) return NextResponse.json({ ok: false, error: `unsafe path in zip: ${e.entryName}` }, { status: 400 });
        const data = e.getData();
        if (data.length > MAX_FILE_BYTES) return NextResponse.json({ ok: false, error: `entry too large: ${safe}` }, { status: 413 });
        files.push({ path: safe, data });
      }
    } else {
      return NextResponse.json({ ok: false, error: 'unsupported file extension (expected .md or .zip)' }, { status: 400 });
    }

    const built = buildPayload(files, nameOverride);
    if (!built.ok) return NextResponse.json({ ok: false, error: built.error }, { status: 400 });
    {
      const c = nameConflict(built.payload.name);
      if (c.conflict) {
        const which = c.source === 'registry' ? 'synced from the marketplace registry' : 'already uploaded';
        return NextResponse.json({ ok: false, error: `A skill named "${built.payload.name}" is ${which}. Uninstall it first, or rename your upload (or the SKILL.md frontmatter \`name:\`) before re-uploading.` }, { status: 409 });
      }
    }
    const { targetDir } = writeSkillToDisk(built.payload, projectPath);
    upsertSkillRow(built.payload, projectPath);
    return NextResponse.json({
      ok: true,
      name: built.payload.name,
      version: built.payload.version || '0.1.0',
      target: targetDir,
      files_written: built.payload.files.length,
    });
  }

  return NextResponse.json({ ok: false, error: 'expected JSON or multipart/form-data' }, { status: 400 });
}
