/**
 * `llm-inspector onboard` subcommand.
 *
 * Generates a Claude Code skill + slash command into the user's home dir so
 * the user can run `/llm-inspector:onboard` to walk through a guided setup.
 *
 * Idempotent by default — re-runs without `--force` are no-ops. `--force`
 * overwrites, `--dry-run` writes nothing. Designed to be safe to invoke
 * from npm's `postinstall` script (any error is logged to stderr, not
 * thrown — the postinstall must not fail the install).
 */

import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { detectAll, detectFirst } from "./detect-tools.js";
import { renderCommandOnboard } from "./templates/command-onboard.js";
import { renderSkillOnboard, REQUIRED_PHASE_HEADINGS } from "./templates/skill-onboard.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const DEFAULT_PORT = 25947;
const SKILL_DIR_NAME = "llm-inspector-onboard";
const SKILL_FILE_NAME = "SKILL.md";
// Windows reserves `:` for NTFS alternate data streams, so a file named
// `llm-inspector:onboard.md` becomes an empty `llm-inspector` on disk.
// Use `-` on Windows and `:` on Unix to match the slash-command convention
// (`/llm-inspector:onboard` vs `/llm-inspector-onboard`) within the
// constraints of each platform.
const COMMAND_FILE_NAME =
  process.platform === "win32" ? "llm-inspector-onboard.md" : "llm-inspector:onboard.md";

export type OnboardFlags = {
  force: boolean;
  dryRun: boolean;
  skipProvider: boolean;
  skipToolWire: boolean;
  skillDir: string | null;
};

function parseFlags(argv: readonly string[]): OnboardFlags {
  const flags: OnboardFlags = {
    force: false,
    dryRun: false,
    skipProvider: false,
    skipToolWire: false,
    skillDir: null,
  };
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i];
    switch (arg) {
      case undefined:
        continue;
      case "--force":
        flags.force = true;
        break;
      case "--dry-run":
        flags.dryRun = true;
        break;
      case "--skip-provider":
        flags.skipProvider = true;
        break;
      case "--skip-tool-wire":
        flags.skipToolWire = true;
        break;
      case "--skill-dir": {
        const next = argv[i + 1];
        if (next === undefined) {
          process.stderr.write("llm-inspector onboard: --skill-dir requires a path argument\n");
          process.exit(2);
        }
        flags.skillDir = next;
        i++;
        break;
      }
      case "--help":
      case "-h":
        printHelp();
        process.exit(0);
        break;
      default:
        process.stderr.write(`llm-inspector onboard: unknown flag: ${arg}\n`);
        process.exit(2);
    }
  }
  return flags;
}

function printHelp(): void {
  process.stdout.write(`llm-inspector onboard — install the llm-inspector Claude Code skill

Usage:
  llm-inspector onboard [options]

Options:
  --force              Overwrite the existing skill and slash command if they exist
  --dry-run            Print target paths and a template preview, write nothing
  --skip-provider      Skip the provider-setup phase in the skill body
  --skip-tool-wire     Skip the wire-tool phase in the skill body
  --skill-dir <path>   Override the target skill directory (default: ~/.claude/skills)
  -h, --help           Show this help

Exit codes:
  0  success (or already installed)
  2  invalid arguments
  1  write failed
`);
}

function resolveTargets(flags: OnboardFlags): {
  skillFile: string;
  commandFile: string;
} {
  const claudeRoot = flags.skillDir ?? join(homedir(), ".claude");
  const skillDir = join(claudeRoot, "skills", SKILL_DIR_NAME);
  const commandsDir = join(claudeRoot, "commands");
  return {
    skillFile: join(skillDir, SKILL_FILE_NAME),
    commandFile: join(commandsDir, COMMAND_FILE_NAME),
  };
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function buildDetectedSummary(): string {
  const entries = detectAll();
  const present = entries.filter((e) => e.result.found);
  const absent = entries.filter((e) => !e.result.found);
  const presentList = present.map((e) => e.displayName).join(", ") || "(none)";
  const absentList = absent.map((e) => e.displayName).join(", ") || "(none)";
  return `  - Detected: ${presentList}\n  - Not detected: ${absentList}`;
}

export function runOnboard(argv: readonly string[]): Promise<number> {
  try {
    return Promise.resolve(runOnboardSync(argv));
  } catch (err) {
    // Last-resort safety net. The postinstall hook relies on this never
    // surfacing — log to stderr and exit 0 so npm install keeps working.
    const msg = err instanceof Error ? err.message : String(err);
    process.stderr.write(`llm-inspector onboard: ${msg}\n`);
    process.stderr.write(
      "(postinstall skill install skipped — run `llm-inspector onboard` later)\n",
    );
    return Promise.resolve(0);
  }
}

function runOnboardSync(argv: readonly string[]): number {
  const flags = parseFlags(argv);
  const { skillFile, commandFile } = resolveTargets(flags);

  // Idempotency check — only relevant for the skill file. If the slash
  // command file is missing but the skill is present, that's weird; treat
  // it the same as "already installed" so a partial state doesn't get
  // half-rewritten on the next run.
  if (!flags.force && existsSync(skillFile)) {
    process.stdout.write(`llm-inspector onboard: already installed at ${skillFile}\n`);
    process.stdout.write("Re-run with --force to refresh.\n");
    return 0;
  }

  // Version stamp — read from package.json so the skill and the published
  // package can never disagree about which version wrote them.
  let version = "0.0.0";
  try {
    const raw: unknown = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
    if (isObject(raw) && typeof raw["version"] === "string") {
      version = raw["version"];
    }
  } catch {
    // best-effort: leave version as the fallback
  }

  const detectedSummary = buildDetectedSummary();
  const skillBody = renderSkillOnboard({
    version,
    port: DEFAULT_PORT,
    detectedSummary,
  });
  const commandBody = renderCommandOnboard();

  if (flags.dryRun) {
    process.stdout.write(`llm-inspector onboard --dry-run\n\n`);
    process.stdout.write(`Skill target:     ${skillFile}\n`);
    process.stdout.write(`Command target:   ${commandFile}\n\n`);
    process.stdout.write(`Skill preview (first 5 lines + headings):\n`);
    const previewLines = skillBody.split("\n").slice(0, 5);
    process.stdout.write(`${previewLines.join("\n")}\n`);
    process.stdout.write(`...\n`);
    for (const heading of REQUIRED_PHASE_HEADINGS) {
      process.stdout.write(`  - ${heading}\n`);
    }
    process.stdout.write(`\nDetected tools:\n${detectedSummary}\n`);
    process.stdout.write(`\nNo files were written.\n`);
    return 0;
  }

  // Ensure target dirs exist. mkdirSync with recursive: true is a no-op if
  // the dir already exists, and on failure it throws — caught by the
  // outer try/catch and reported as a soft postinstall skip.
  mkdirSync(join(skillFile, ".."), { recursive: true });
  mkdirSync(join(commandFile, ".."), { recursive: true });

  try {
    writeFileSync(skillFile, skillBody, "utf8");
    writeFileSync(commandFile, commandBody, "utf8");
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    process.stderr.write(`llm-inspector onboard: failed to write files: ${msg}\n`);
    return 1;
  }

  const firstTool = detectFirst();
  const toolHint = firstTool !== null ? firstTool.displayName : "no known tool detected";

  process.stdout.write(`Installed skill to:   ${skillFile}\n`);
  process.stdout.write(`Installed command to: ${commandFile}\n`);
  process.stdout.write(`\nNext steps:\n`);
  process.stdout.write(`  - Open Claude Code and run: /llm-inspector:onboard\n`);
  process.stdout.write(`  - Or refresh later: llm-inspector onboard --force\n`);
  process.stdout.write(`  - Detected primary tool: ${toolHint}\n`);
  return 0;
}
