import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { isAbsolute, join, relative } from "node:path";
import { repoRoot, rootPath } from "./config";
import type { RenderedCliResult } from "./output";

const DEFAULT_SKILLS_DIR = ".agents/skills";
const DEFAULT_SKILL_MD_LIMIT_BYTES = 10 * 1024;

interface HealthOptions {
  skillsDir: string;
  thresholdBytes: number;
  json: boolean;
  full: boolean;
}

interface SkillHealthItem {
  skillId: string;
  skillPath: string;
  skillMdPath: string;
  referencesDir: string;
  bytes: number;
  lines: number;
  limitBytes: number;
  overBytes: number;
  status: "OK" | "ALERT";
  action: string;
}

interface SkillHealthResult {
  ok: boolean;
  command: "health";
  check: "skill-md-size";
  thresholdBytes: number;
  skillsDir: string;
  summary: {
    totalSkills: number;
    okSkills: number;
    alertSkills: number;
    maxBytes: number;
    maxSkillId: string | null;
  };
  alerts: SkillHealthItem[];
  skills: SkillHealthItem[];
  policy: {
    limitBytes: number;
    splitTarget: ".agents/skills/<skill_id>/references/*.md";
    skillMdRole: string;
  };
  next: string[];
}

export function healthHelp(): Record<string, unknown> {
  return {
    command: "health",
    output: "text by default; use --json, --full, or --raw for structured output",
    usage: [
      "bun scripts/cli.ts health",
      "bun scripts/cli.ts health --json",
      "bun scripts/cli.ts health --skills-dir .agents/skills --threshold-bytes 10240",
    ],
    checks: [
      {
        name: "skill-md-size",
        defaultThresholdBytes: DEFAULT_SKILL_MD_LIMIT_BYTES,
        scope: ".agents/skills/*/SKILL.md",
        failure: "SKILL.md over threshold",
        action: "move lower-frequency detail into .agents/skills/<skill_id>/references/*.md and leave SKILL.md as high-frequency routing/workflow guidance plus reference links",
      },
    ],
  };
}

export function runHealthCommand(args: string[]): SkillHealthResult | RenderedCliResult {
  const options = parseHealthOptions(args);
  const result = scanSkillHealth(options);
  if (options.json || options.full) return result;
  return {
    ok: result.ok,
    command: "health",
    contentType: "text/plain",
    renderedText: renderSkillHealth(result),
  };
}

function parseHealthOptions(args: string[]): HealthOptions {
  const options: HealthOptions = {
    skillsDir: DEFAULT_SKILLS_DIR,
    thresholdBytes: DEFAULT_SKILL_MD_LIMIT_BYTES,
    json: false,
    full: false,
  };
  for (let index = 0; index < args.length; index += 1) {
    const arg = args[index];
    if (arg === "--json") {
      options.json = true;
    } else if (arg === "--full" || arg === "--raw") {
      options.full = true;
      options.json = true;
    } else if (arg === "--skills-dir") {
      options.skillsDir = stringArg(arg, args[index + 1]);
      index += 1;
    } else if (arg === "--threshold-bytes") {
      options.thresholdBytes = positiveIntArg(arg, args[index + 1], 1024 * 1024);
      index += 1;
    } else {
      throw new Error(`unknown health option: ${arg}`);
    }
  }
  return options;
}

function stringArg(name: string, raw: string | undefined): string {
  if (raw === undefined || raw.trim().length === 0) throw new Error(`${name} requires a non-empty value`);
  return raw;
}

function positiveIntArg(name: string, raw: string | undefined, max: number): number {
  const value = Number(raw);
  if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
  return Math.min(value, max);
}

function resolvePath(path: string): string {
  return isAbsolute(path) ? path : rootPath(path);
}

function displayPath(path: string): string {
  const rel = relative(repoRoot, path);
  return rel.length > 0 && !rel.startsWith("..") ? rel : path;
}

function scanSkillHealth(options: HealthOptions): SkillHealthResult {
  const skillsRoot = resolvePath(options.skillsDir);
  if (!existsSync(skillsRoot)) throw new Error(`health skills directory not found: ${displayPath(skillsRoot)}`);
  const skillDirs = readdirSync(skillsRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .map((entry) => join(skillsRoot, entry.name))
    .filter((dir) => existsSync(join(dir, "SKILL.md")))
    .sort();

  const skills = skillDirs.map((skillPath) => skillHealthItem(skillPath, options.thresholdBytes));
  const alerts = skills.filter((item) => item.status === "ALERT");
  const max = skills.reduce<SkillHealthItem | null>((current, item) => current === null || item.bytes > current.bytes ? item : current, null);
  const ok = skills.length > 0 && alerts.length === 0;
  return {
    ok,
    command: "health",
    check: "skill-md-size",
    thresholdBytes: options.thresholdBytes,
    skillsDir: displayPath(skillsRoot),
    summary: {
      totalSkills: skills.length,
      okSkills: skills.length - alerts.length,
      alertSkills: alerts.length,
      maxBytes: max?.bytes ?? 0,
      maxSkillId: max?.skillId ?? null,
    },
    alerts,
    skills,
    policy: {
      limitBytes: options.thresholdBytes,
      splitTarget: ".agents/skills/<skill_id>/references/*.md",
      skillMdRole: "Keep SKILL.md under the limit with only trigger-critical, highest-frequency workflow guidance; move lower-frequency detail into directly linked references.",
    },
    next: skills.length === 0
      ? [`No SKILL.md files were found under ${displayPath(skillsRoot)}; check --skills-dir.`]
      : alerts.length === 0
      ? ["No skill split is required."]
      : [
          "Move lower-frequency detail into .agents/skills/<skill_id>/references/*.md.",
          "Keep SKILL.md as a compact high-frequency workflow and navigation index.",
          "Rerun: bun scripts/cli.ts health",
        ],
  };
}

function skillHealthItem(skillPath: string, limitBytes: number): SkillHealthItem {
  const skillMdPath = join(skillPath, "SKILL.md");
  const text = readFileSync(skillMdPath, "utf8");
  const bytes = statSync(skillMdPath).size;
  const skillId = skillPath.split(/[\\/]/u).pop() ?? displayPath(skillPath);
  const referencesDir = join(skillPath, "references");
  const overBytes = Math.max(0, bytes - limitBytes);
  return {
    skillId,
    skillPath: displayPath(skillPath),
    skillMdPath: displayPath(skillMdPath),
    referencesDir: displayPath(referencesDir),
    bytes,
    lines: text.split(/\r?\n/u).length,
    limitBytes,
    overBytes,
    status: overBytes > 0 ? "ALERT" : "OK",
    action: overBytes > 0 ? `split lower-frequency detail to ${displayPath(referencesDir)}` : "-",
  };
}

function renderSkillHealth(result: SkillHealthResult): string {
  const lines = [
    `skill health (${result.ok ? "ok" : "alert"})`,
    "",
    renderTable(["SUMMARY", "VALUE"], [
      ["skills", String(result.summary.totalSkills)],
      ["limit", `${result.thresholdBytes} bytes`],
      ["alerts", String(result.summary.alertSkills)],
      ["largest", result.summary.maxSkillId === null ? "-" : `${result.summary.maxSkillId} ${result.summary.maxBytes} bytes`],
    ]),
    "",
  ];
  if (result.alerts.length > 0) {
    lines.push(
      "ALERTS",
      renderTable(["SKILL", "BYTES", "OVER", "LINES", "ACTION"], result.alerts.map((item) => [
        item.skillId,
        String(item.bytes),
        String(item.overBytes),
        String(item.lines),
        item.action,
      ])),
      "",
    );
  } else {
    lines.push("ALERTS", "  none", "");
  }
  lines.push(
    "Policy:",
    `  SKILL.md limit: ${result.thresholdBytes} bytes`,
    "  Split target: .agents/skills/<skill_id>/references/*.md",
    "  Keep SKILL.md to highest-frequency workflow/routing content and direct reference links.",
    "",
    "Next:",
    ...result.next.map((item) => `  ${item}`),
    "  bun scripts/cli.ts health --full",
  );
  return lines.join("\n");
}

function renderTable(headers: string[], rows: string[][]): string {
  const normalized = rows.length === 0 ? [headers.map(() => "-")] : rows;
  const widths = headers.map((header, index) => Math.max(header.length, ...normalized.map((row) => (row[index] ?? "").length)));
  const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join("  ").trimEnd();
  return [renderRow(headers), ...normalized.map(renderRow)].join("\n");
}
