import * as path from 'path';

import { cp, ensureDir, fileExists, readdir, readFile, writeFile } from '@frontmcp/utils';

import { c } from '../../core/colors';
import { getSelfVersion } from '../../core/version';
import { resolveEntry } from '../../shared/fs';
import { assertValidPluginName } from '../build/exec/cli-runtime/plugin-emitter';
import type { ExtractedSkillAsset } from '../build/exec/cli-runtime/schema-extractor';
import { composeSkillMd, hasFrontmatter } from '../build/exec/cli-runtime/skill-md-compose';
import { getCatalogDir, loadCatalog } from './catalog';
import { extractProjectSkills, resolvePackageEntry } from './from-entry';

const PROVIDER_DIRS: Record<string, string> = {
  claude: '.claude/skills',
  codex: '.codex/skills',
};

/** Start marker for the auto-generated skills block (version is appended). */
const SKILLS_BLOCK_START = '<!-- frontmcp:skills-start';
/** End marker for the auto-generated skills block. */
const SKILLS_BLOCK_END = '<!-- frontmcp:skills-end -->';
/** Legacy marker from older versions (used for migration). */
const LEGACY_SKILLS_MARKER = '# Skills and Tools';

/**
 * Build a short summary from a skill description.
 * Takes the first sentence or truncates at ~80 chars.
 */
function summarizeDescription(description: string): string {
  // Take text before the first period that ends a sentence
  const firstSentence = description.replace(/\. .+$/, '');
  // Remove the "Use when you want to" / "Use when you need to" prefix
  const cleaned = firstSentence
    .replace(/^Use when you (?:want|need) to\s+/i, '')
    .replace(/^Use (?:before|when)\s+/i, '');
  // Truncate if still long
  if (cleaned.length > 90) {
    return cleaned.slice(0, 87) + '...';
  }
  return cleaned;
}

/**
 * Build the skills CLAUDE.md section dynamically from the manifest.
 */
export function buildSkillsSection(version: string, skills: { name: string; description: string }[]): string {
  const lines: string[] = [];

  lines.push(`${SKILLS_BLOCK_START} v${version} -->`);
  lines.push('# Skills and Tools');
  lines.push('');
  lines.push(`> Auto-generated by \`frontmcp skills install\` (v${version}) — do not edit manually.`);
  lines.push('');
  lines.push('This project uses **FrontMCP skills** installed in `.claude/skills/`.');
  lines.push('Before writing code, search the installed skills for relevant guidance:');
  lines.push('');

  for (const skill of skills) {
    const summary = summarizeDescription(skill.description);
    lines.push(`- **${skill.name}** — ${summary}`);
  }

  lines.push('');
  lines.push(
    'When you need to implement something, **read the matching skill first** — it contains patterns, examples, and common mistakes to avoid.',
  );
  lines.push(SKILLS_BLOCK_END);
  lines.push('');

  return lines.join('\n');
}

export interface InstallOptions {
  provider?: 'claude' | 'codex';
  dir?: string;
  all?: boolean;
  tag?: string;
  category?: string;
  /**
   * Install skills from a project's `@Skill`-decorated entries instead of
   * the bundled framework catalog. Bundles the entry via esbuild and
   * enumerates skills through the same path the per-bin install uses.
   */
  fromEntry?: string;
  /**
   * Install skills from a published package's `@Skill`-decorated entries.
   * The package must be installed under the project's `node_modules/` so
   * Node's module resolution can locate it.
   */
  fromPackage?: string;
}

/**
 * Ensure CLAUDE.md exists and contains an up-to-date skills section.
 *
 * - If markers exist: replaces the block between them (handles version updates).
 * - If legacy `# Skills and Tools` header exists (no markers): replaces it.
 * - If neither exists: prepends the block.
 * - If file doesn't exist: creates it with the block.
 */
export async function ensureClaudeMdSkillsInstructions(cwd: string): Promise<void> {
  const version = getSelfVersion();
  // Scan the actual `.claude/skills/<name>/SKILL.md` tree rather than
  // filtering against the framework catalog — project-defined skills
  // (installed via `--from-entry` / `--from-package`) live in the same
  // directory and must also be listed in the CLAUDE.md block.
  const installedSkills = await scanInstalledSkills(path.join(cwd, '.claude', 'skills'));
  const section = buildSkillsSection(version, installedSkills);
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');

  if (await fileExists(claudeMdPath)) {
    let content = await readFile(claudeMdPath);

    if (content.includes(SKILLS_BLOCK_START)) {
      // Replace existing marker-bounded block
      const startIdx = content.indexOf(SKILLS_BLOCK_START);
      const endIdx = content.indexOf(SKILLS_BLOCK_END, startIdx);
      if (endIdx !== -1) {
        const before = content.slice(0, startIdx);
        const after = content.slice(endIdx + SKILLS_BLOCK_END.length);
        // Trim leading newlines from after to avoid accumulating blank lines
        content = before + section + after.replace(/^\n+/, '\n');
      } else {
        // Malformed: start marker without end — replace from start marker to end of file section
        content = content.slice(0, startIdx) + section + '\n';
      }
    } else if (content.includes(LEGACY_SKILLS_MARKER)) {
      // Migrate legacy format: replace from the legacy header to the next markdown heading
      const legacyIdx = content.indexOf(LEGACY_SKILLS_MARKER);
      const before = content.slice(0, legacyIdx);
      const afterLegacy = content.slice(legacyIdx + LEGACY_SKILLS_MARKER.length);
      // Find the next markdown heading (# at start of line) after the legacy header
      const nextHeadingMatch = afterLegacy.match(/\n(?=#{1,6}\s)/);
      const after =
        nextHeadingMatch && nextHeadingMatch.index !== undefined ? afterLegacy.slice(nextHeadingMatch.index) : '';
      content = before + section + after;
    } else {
      // No existing skills section — prepend
      content = section + '\n' + content;
    }

    await writeFile(claudeMdPath, content);
    console.log(`${c('green', '✓')} Updated ${c('cyan', 'CLAUDE.md')} with skills usage instructions`);
  } else {
    await writeFile(claudeMdPath, section);
    console.log(`${c('green', '✓')} Created ${c('cyan', 'CLAUDE.md')} with skills usage instructions`);
  }
}

export async function installSkill(name: string | undefined, options: InstallOptions): Promise<void> {
  const provider = options.provider ?? 'claude';
  const targetBase = options.dir ?? path.resolve(process.cwd(), PROVIDER_DIRS[provider] ?? PROVIDER_DIRS['claude']);

  if (options.fromEntry && options.fromPackage) {
    console.error(c('red', 'Options --from-entry and --from-package are mutually exclusive.'));
    process.exit(1);
  }

  if (options.fromEntry || options.fromPackage) {
    await installFromProject({ name, options, provider, targetBase });
    return;
  }

  const manifest = loadCatalog();
  const catalogDir = getCatalogDir();

  // Validate that exactly one selector is supplied
  const selectorCount = [options.all, options.tag, options.category, name].filter(Boolean).length;
  if (selectorCount > 1) {
    console.error(c('red', 'Options --all, --tag, --category, and <name> are mutually exclusive.'));
    console.log(c('gray', 'Provide exactly one selector to choose which skills to install.'));
    process.exit(1);
  }

  // Determine which skills to install
  let skills = manifest.skills;

  if (options.all) {
    // Install all skills
  } else if (options.tag) {
    const tag = options.tag;
    skills = skills.filter((s) => s.tags.includes(tag));
    if (skills.length === 0) {
      console.error(c('red', `No skills found with tag "${tag}".`));
      console.log(c('gray', "Use 'frontmcp skills list --tag <tag>' to see available tags."));
      process.exit(1);
    }
  } else if (options.category) {
    skills = skills.filter((s) => s.category === options.category);
    if (skills.length === 0) {
      console.error(c('red', `No skills found in category "${options.category}".`));
      console.log(c('gray', "Use 'frontmcp skills list' to see available categories."));
      process.exit(1);
    }
  } else if (name) {
    // Single skill install
    const entry = skills.find((s) => s.name === name);
    if (!entry) {
      console.error(c('red', `Skill "${name}" not found in catalog.`));
      console.log(c('gray', "Use 'frontmcp skills list' to see available skills."));
      process.exit(1);
    }
    skills = [entry];
  } else {
    console.error(c('red', 'Please specify a skill name, or use --all, --tag, or --category.'));
    process.exit(1);
  }

  // Install each skill
  let installed = 0;
  for (const entry of skills) {
    const targetDir = path.join(targetBase, entry.name);
    const sourceDir = path.join(catalogDir, entry.path);

    if (!(await fileExists(path.join(sourceDir, 'SKILL.md')))) {
      console.error(c('yellow', `  Skipped ${entry.name}: source SKILL.md not found`));
      continue;
    }

    await ensureDir(targetDir);
    await cp(sourceDir, targetDir, { recursive: true });
    installed++;

    console.log(
      `${c('green', '✓')} Installed ${c('bold', entry.name)} to ${c('cyan', path.relative(process.cwd(), targetDir))}`,
    );
  }

  if (installed === 0) {
    console.error(c('red', `No skills were installed (0/${skills.length} had valid SKILL.md files).`));
    process.exit(1);
  }

  if (skills.length > 1) {
    console.log(`\n${c('green', '✓')} Installed ${installed}/${skills.length} skills (provider: ${provider})`);
  }

  // For Claude provider: ensure CLAUDE.md has skills usage instructions
  if (provider === 'claude') {
    await ensureClaudeMdSkillsInstructions(process.cwd());
  }
}

interface InstallFromProjectArgs {
  name: string | undefined;
  options: InstallOptions;
  provider: 'claude' | 'codex';
  targetBase: string;
}

async function installFromProject(args: InstallFromProjectArgs): Promise<void> {
  const { name, options, provider, targetBase } = args;

  const selectorCount = [options.all, name].filter(Boolean).length;
  if (selectorCount > 1) {
    console.error(c('red', 'Options --all and <name> are mutually exclusive.'));
    process.exit(1);
  }
  if (selectorCount === 0) {
    console.error(c('red', 'Specify a skill name or --all when using --from-entry/--from-package.'));
    process.exit(1);
  }
  if (options.tag || options.category) {
    console.error(c('red', '--tag and --category are not supported with --from-entry/--from-package yet.'));
    process.exit(1);
  }

  const cwd = process.cwd();
  let entry: string;
  if (options.fromPackage) {
    try {
      entry = resolvePackageEntry(options.fromPackage, cwd);
    } catch (err) {
      console.error(
        c('red', `Could not resolve package "${options.fromPackage}" from ${cwd}: ${(err as Error).message}`),
      );
      process.exit(1);
    }
  } else {
    entry = await resolveEntry(cwd, options.fromEntry);
  }

  let assets: ExtractedSkillAsset[];
  try {
    assets = await extractProjectSkills({ entry, cwd });
  } catch (err) {
    console.error(c('red', `Could not enumerate @Skill entries from ${entry}: ${(err as Error).message}`));
    process.exit(1);
  }

  if (assets.length === 0) {
    console.error(c('yellow', `No @Skill-decorated entries found in ${path.relative(cwd, entry) || entry}.`));
    process.exit(1);
  }

  let selected = assets;
  if (name) {
    const match = assets.find((a) => a.skillName === name);
    if (!match) {
      console.error(c('red', `Skill "${name}" not found in ${options.fromPackage ?? path.relative(cwd, entry)}.`));
      console.log(c('gray', `  Available: ${assets.map((a) => a.skillName).join(', ')}`));
      process.exit(1);
    }
    selected = [match];
  }

  let installed = 0;
  for (const asset of selected) {
    // Validate the skill name BEFORE it lands in a filesystem path or
    // the synthesized SKILL.md frontmatter. Same rules as plugin names
    // (issue #411 security pass) — a malicious `@Skill({ name: '../x' })`
    // would otherwise escape `targetBase` via `path.join`.
    try {
      assertValidPluginName(asset.skillName, 'skills install --from-entry');
    } catch (err) {
      console.error(c('yellow', `  Skipped: ${(err as Error).message}`));
      continue;
    }
    const targetDir = path.join(targetBase, asset.skillName);
    try {
      await materializeProjectSkill(asset, targetDir);
      installed++;
      console.log(
        `${c('green', '✓')} Installed ${c('bold', asset.skillName)} to ${c('cyan', path.relative(cwd, targetDir))}`,
      );
    } catch (err) {
      console.error(c('yellow', `  Skipped ${asset.skillName}: ${(err as Error).message}`));
    }
  }

  if (installed === 0) {
    console.error(c('red', `No skills were installed (0/${selected.length}).`));
    process.exit(1);
  }

  if (selected.length > 1) {
    console.log(`\n${c('green', '✓')} Installed ${installed}/${selected.length} skills (provider: ${provider})`);
  }

  if (provider === 'claude') {
    await ensureClaudeMdSkillsInstructions(cwd);
  }
}

async function materializeProjectSkill(asset: ExtractedSkillAsset, targetDir: string): Promise<void> {
  await ensureDir(targetDir);

  const body =
    asset.instructionFile && (await fileExists(asset.instructionFile)) ? await readFile(asset.instructionFile) : '';
  const skillMd = composeSkillMd(
    { name: asset.skillName, description: asset.description, tags: asset.tags, license: asset.license },
    body,
  );
  await writeFile(path.join(targetDir, 'SKILL.md'), skillMd);

  for (const kind of ['references', 'examples', 'scripts', 'assets'] as const) {
    const src = asset.resourceDirs?.[kind];
    if (!src) continue;
    if (!(await fileExists(src))) continue;
    const dest = path.join(targetDir, kind);
    await cp(src, dest, { recursive: true });
  }
}

interface InstalledSkillSummary {
  name: string;
  description: string;
}

/**
 * Walk `<base>/<skill>/SKILL.md` and read the frontmatter `name` / `description`
 * for each. Used by `ensureClaudeMdSkillsInstructions` to list both catalog
 * and project-defined skills in the auto-generated block.
 */
async function scanInstalledSkills(base: string): Promise<InstalledSkillSummary[]> {
  if (!(await fileExists(base))) return [];
  const catalog = loadCatalog();
  const catalogByName = new Map(catalog.skills.map((s) => [s.name, s] as const));

  let entries: string[];
  try {
    entries = await readdir(base);
  } catch {
    return [];
  }

  const summaries: InstalledSkillSummary[] = [];
  for (const entry of entries.sort()) {
    const skillMdPath = path.join(base, entry, 'SKILL.md');
    if (!(await fileExists(skillMdPath))) continue;

    const fromCatalog = catalogByName.get(entry);
    if (fromCatalog) {
      summaries.push({ name: fromCatalog.name, description: fromCatalog.description });
      continue;
    }

    // Project-defined skill — read frontmatter for description.
    const content = await readFile(skillMdPath);
    summaries.push({ name: entry, description: extractFrontmatterDescription(content) ?? `${entry} skill` });
  }
  return summaries;
}

function extractFrontmatterDescription(content: string): string | undefined {
  if (!hasFrontmatter(content)) return undefined;
  const end = content.indexOf('\n---', 4);
  if (end < 0) return undefined;
  const frontmatter = content.slice(4, end);
  const match = frontmatter.match(/^description:\s*(.+)$/m);
  if (!match) return undefined;
  const raw = match[1].trim();
  if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
    try {
      return JSON.parse(raw.replace(/^'/, '"').replace(/'$/, '"'));
    } catch {
      return raw.slice(1, -1);
    }
  }
  return raw;
}
