import {
  existsSync,
  lstatSync,
  mkdirSync,
  readdirSync,
  readlinkSync,
  symlinkSync,
  unlinkSync,
  type Stats,
} from 'node:fs';
import { homedir } from 'node:os';
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
import type { Command } from 'commander';
import { DEFAULT_MOSAIC_HOME } from '../constants.js';

export interface SkillPaths {
  mosaicSkillsDir: string;
  claudeSkillsDir: string;
}

export type SkillRegistrationStatus = 'registered' | 'already-registered' | 'repaired';
export type SkillUnregistrationStatus = 'unregistered' | 'already-unregistered';
export type SkillListStatus =
  | 'registered'
  | 'unregistered'
  | 'dangling'
  | 'foreign'
  | 'foreign-dangling'
  | 'misdirected';

export interface SkillRegistrationResult {
  name: string;
  status: SkillRegistrationStatus;
  sourcePath: string;
  linkPath: string;
}

export interface SkillUnregistrationResult {
  name: string;
  status: SkillUnregistrationStatus;
  linkPath: string;
}

export interface SkillListEntry {
  name: string;
  status: SkillListStatus;
  sourcePath?: string;
  linkPath: string;
  targetPath?: string;
}

export interface SkillSyncConflict {
  name: string;
  reason: string;
}

export interface SkillSyncResult {
  registered: string[];
  repaired: string[];
  unchanged: string[];
  conflicts: SkillSyncConflict[];
}

const SAFE_SKILL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;

export class SkillBridgeError extends Error {
  public constructor(message: string) {
    super(message);
    this.name = 'SkillBridgeError';
  }
}

/** Resolve the production bridge paths while keeping tests injectable. */
export function getDefaultSkillPaths(): SkillPaths {
  const mosaicHome = process.env['MOSAIC_HOME'] ?? DEFAULT_MOSAIC_HOME;
  const claudeHome = process.env['CLAUDE_HOME'] ?? join(homedir(), '.claude');
  return {
    mosaicSkillsDir: join(mosaicHome, 'skills'),
    claudeSkillsDir: join(claudeHome, 'skills'),
  };
}

/**
 * Reject a user-supplied name before any filesystem operation.
 * A skill name must identify one direct child in both managed roots.
 */
export function validateSkillName(name: string): void {
  if (
    name.length === 0 ||
    name.startsWith('-') ||
    name.endsWith('.') ||
    name.includes('..') ||
    name.includes('/') ||
    name.includes('\\') ||
    isAbsolute(name) ||
    !SAFE_SKILL_NAME.test(name)
  ) {
    throw new SkillBridgeError(
      `Invalid skill name ${JSON.stringify(name)}: use letters, numbers, dots, underscores, or hyphens; start with a letter or number; and do not use paths, "..", or a leading "-".`,
    );
  }
}

function displaySkillName(name: string): string {
  return SAFE_SKILL_NAME.test(name) ? name : JSON.stringify(name);
}

function lstatIfPresent(path: string): Stats | undefined {
  try {
    return lstatSync(path);
  } catch (error: unknown) {
    if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return undefined;
    throw error;
  }
}

function assertNoSymlinkAncestors(path: string): void {
  const absolute = resolve(path);
  const pathRoot = parse(absolute).root;
  let current = pathRoot;

  for (const segment of relative(pathRoot, absolute).split(sep)) {
    if (segment.length === 0) continue;
    current = join(current, segment);
    const entry = lstatIfPresent(current);
    if (!entry) break;
    if (entry.isSymbolicLink()) {
      throw new SkillBridgeError(
        `Refusing symlink ancestor at ${current}; managed skill roots must resolve without symlink traversal.`,
      );
    }
  }
}

function assertManagedRoots(paths: SkillPaths): void {
  assertNoSymlinkAncestors(paths.mosaicSkillsDir);
  assertNoSymlinkAncestors(paths.claudeSkillsDir);
}

function directChild(root: string, name: string): string {
  const resolvedRoot = resolve(root);
  const child = resolve(resolvedRoot, name);
  if (dirname(child) !== resolvedRoot) {
    throw new SkillBridgeError(`Invalid skill name "${name}": resolved path escapes its root.`);
  }
  return child;
}

function resolveLinkTarget(linkPath: string): string {
  return resolve(dirname(linkPath), readlinkSync(linkPath));
}

function isInsideSkillsRoot(targetPath: string, skillsRoot: string): boolean {
  const rel = relative(resolve(skillsRoot), resolve(targetPath));
  return rel.length > 0 && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
}

function isDangling(linkPath: string): boolean {
  return !existsSync(linkPath);
}

function assertSourceSkill(name: string, paths: SkillPaths): string {
  const sourcePath = directChild(paths.mosaicSkillsDir, name);
  const source = lstatIfPresent(sourcePath);
  if (!source?.isDirectory()) {
    throw new SkillBridgeError(
      `Canonical skill directory not found: ${sourcePath}. Add the skill under the Mosaic skills directory before registering it.`,
    );
  }
  return sourcePath;
}

function foreignTargetError(linkPath: string): SkillBridgeError {
  return new SkillBridgeError(
    `Refusing to modify foreign entry at ${linkPath}; only symlinks pointing inside the Mosaic skills directory are managed.`,
  );
}

/** Register one canonical skill with Claude Code without clobbering foreign entries. */
export function registerSkill(
  name: string,
  paths: SkillPaths = getDefaultSkillPaths(),
): SkillRegistrationResult {
  validateSkillName(name);
  assertManagedRoots(paths);
  const sourcePath = assertSourceSkill(name, paths);
  const linkPath = directChild(paths.claudeSkillsDir, name);
  const existing = lstatIfPresent(linkPath);

  if (!existing) {
    mkdirSync(paths.claudeSkillsDir, { recursive: true });
    symlinkSync(sourcePath, linkPath);
    return { name, status: 'registered', sourcePath, linkPath };
  }

  if (!existing.isSymbolicLink()) throw foreignTargetError(linkPath);

  const existingTarget = resolveLinkTarget(linkPath);
  if (!isInsideSkillsRoot(existingTarget, paths.mosaicSkillsDir)) {
    throw foreignTargetError(linkPath);
  }

  if (existingTarget === resolve(sourcePath) && !isDangling(linkPath)) {
    return { name, status: 'already-registered', sourcePath, linkPath };
  }

  if (!isDangling(linkPath)) {
    throw new SkillBridgeError(
      `Refusing to replace live Mosaic skill symlink at ${linkPath}; it points to ${existingTarget}, not ${sourcePath}.`,
    );
  }

  unlinkSync(linkPath);
  symlinkSync(sourcePath, linkPath);
  return { name, status: 'repaired', sourcePath, linkPath };
}

/** Unregister only a symlink owned by the canonical Mosaic skills root. */
export function unregisterSkill(
  name: string,
  paths: SkillPaths = getDefaultSkillPaths(),
): SkillUnregistrationResult {
  validateSkillName(name);
  assertManagedRoots(paths);
  const linkPath = directChild(paths.claudeSkillsDir, name);
  const existing = lstatIfPresent(linkPath);

  if (!existing) return { name, status: 'already-unregistered', linkPath };
  if (!existing.isSymbolicLink()) throw foreignTargetError(linkPath);

  const targetPath = resolveLinkTarget(linkPath);
  if (!isInsideSkillsRoot(targetPath, paths.mosaicSkillsDir)) throw foreignTargetError(linkPath);

  const expectedTarget = resolve(directChild(paths.mosaicSkillsDir, name));
  if (targetPath !== expectedTarget) {
    throw new SkillBridgeError(
      `Refusing to unregister misdirected Mosaic skill symlink at ${linkPath}; it points to ${targetPath}, not ${expectedTarget}.`,
    );
  }

  unlinkSync(linkPath);
  return { name, status: 'unregistered', linkPath };
}

function canonicalSkillNames(paths: SkillPaths): string[] {
  assertNoSymlinkAncestors(paths.mosaicSkillsDir);
  const root = lstatIfPresent(paths.mosaicSkillsDir);
  if (!root) return [];
  if (!root.isDirectory()) {
    throw new SkillBridgeError(
      `Canonical skills path is not a directory: ${paths.mosaicSkillsDir}`,
    );
  }
  return readdirSync(paths.mosaicSkillsDir, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => entry.name)
    .sort();
}

function claudeEntryNames(paths: SkillPaths): string[] {
  assertNoSymlinkAncestors(paths.claudeSkillsDir);
  const root = lstatIfPresent(paths.claudeSkillsDir);
  if (!root) return [];
  if (!root.isDirectory()) {
    throw new SkillBridgeError(`Claude skills path is not a directory: ${paths.claudeSkillsDir}`);
  }
  return readdirSync(paths.claudeSkillsDir)
    .filter((name) => name.length > 0)
    .sort();
}

/** Return a deterministic union of canonical skills and Claude bridge entries. */
export function listSkills(paths: SkillPaths = getDefaultSkillPaths()): SkillListEntry[] {
  const canonicalNames = new Set(canonicalSkillNames(paths));
  const names = new Set([...canonicalNames, ...claudeEntryNames(paths)]);
  const entries: SkillListEntry[] = [];

  for (const name of [...names].sort()) {
    const sourcePath = canonicalNames.has(name)
      ? directChild(paths.mosaicSkillsDir, name)
      : undefined;
    const linkPath = directChild(paths.claudeSkillsDir, name);
    const installed = lstatIfPresent(linkPath);

    if (!installed) {
      if (sourcePath) entries.push({ name, status: 'unregistered', sourcePath, linkPath });
      continue;
    }

    if (!installed.isSymbolicLink()) {
      entries.push({ name, status: 'foreign', sourcePath, linkPath });
      continue;
    }

    const targetPath = resolveLinkTarget(linkPath);
    const owned = isInsideSkillsRoot(targetPath, paths.mosaicSkillsDir);
    const dangling = isDangling(linkPath);

    if (!owned) {
      entries.push({
        name,
        status: dangling ? 'foreign-dangling' : 'foreign',
        sourcePath,
        linkPath,
        targetPath,
      });
      continue;
    }

    if (dangling) {
      entries.push({ name, status: 'dangling', sourcePath, linkPath, targetPath });
      continue;
    }

    entries.push({
      name,
      status: sourcePath && targetPath === resolve(sourcePath) ? 'registered' : 'misdirected',
      sourcePath,
      linkPath,
      targetPath,
    });
  }

  return entries;
}

/** Reconcile every canonical skill directory while preserving all foreign entries. */
export function syncClaudeSkills(paths: SkillPaths = getDefaultSkillPaths()): SkillSyncResult {
  const result: SkillSyncResult = {
    registered: [],
    repaired: [],
    unchanged: [],
    conflicts: [],
  };

  for (const name of canonicalSkillNames(paths)) {
    try {
      const registration = registerSkill(name, paths);
      if (registration.status === 'registered') result.registered.push(name);
      if (registration.status === 'repaired') result.repaired.push(name);
      if (registration.status === 'already-registered') result.unchanged.push(name);
    } catch (error: unknown) {
      result.conflicts.push({
        name: displaySkillName(name),
        reason: error instanceof Error ? error.message : String(error),
      });
    }
  }

  return result;
}

function reportCommandError(error: unknown): void {
  console.error(error instanceof Error ? error.message : String(error));
  process.exitCode = 1;
}

/** Register the `mosaic skill` command group. */
export function registerSkillCommand(
  program: Command,
  paths: SkillPaths = getDefaultSkillPaths(),
): void {
  const skill = program
    .command('skill')
    .description('Manage Claude Code skill registrations')
    .configureHelp({ sortSubcommands: true });

  skill
    .command('register <name>')
    .description('Register a Mosaic skill with Claude Code')
    .action((name: string) => {
      try {
        const result = registerSkill(name, paths);
        if (result.status === 'already-registered') {
          console.log(`${name}: already registered`);
        } else if (result.status === 'repaired') {
          console.log(`${name}: repaired dangling registration`);
        } else {
          console.log(`${name}: registered`);
        }
      } catch (error: unknown) {
        reportCommandError(error);
      }
    });

  skill
    .command('unregister <name>')
    .description('Unregister a Mosaic skill from Claude Code')
    .action((name: string) => {
      try {
        const result = unregisterSkill(name, paths);
        console.log(
          result.status === 'already-unregistered'
            ? `${name}: already unregistered`
            : `${name}: unregistered`,
        );
      } catch (error: unknown) {
        reportCommandError(error);
      }
    });

  skill
    .command('list')
    .description('List registered, dangling, foreign, and unregistered skills')
    .action(() => {
      try {
        const entries = listSkills(paths);
        if (entries.length === 0) {
          console.log('No Mosaic or Claude Code skills found.');
          return;
        }
        for (const entry of entries) {
          console.log(`${entry.status.padEnd(17)} ${displaySkillName(entry.name)}`);
        }
      } catch (error: unknown) {
        reportCommandError(error);
      }
    });
}
