import type { Command } from "commander";
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { resolve, dirname, basename } from "node:path";
import {
  compileAnyFile,
  compileFromIR,
  irFromJSON,
  type AnyCompileResult,
} from "../core/compiler.js";
import type { Diagnostic } from "../core/types.js";
import { renderDiagnostic } from "./render-diagnostics.js";
import { shouldUseColor } from "./color.js";
import {
  printRepairArtifactLines,
  tryEmitRepairArtifacts,
} from "../repair/repair-artifacts.js";

/**
 * Count non-blank lines in a source string. Blank lines (whitespace
 * only) don't carry semantic weight on either the TS or Swift side
 * and would distort the ratio toward whichever input was more
 * generously formatted.
 */
export function countNonBlankLines(source: string): number {
  let count = 0;
  for (const line of source.split("\n")) {
    if (line.trim().length > 0) count++;
  }
  return count;
}

/**
 * Render a Swift-over-TS size ratio (e.g. "0.42x" meaning the Swift
 * output is 42% the size of the TS input, i.e. compressed; "1.50x"
 * meaning the Swift output grew to 150% of the TS input, i.e.
 * expanded). Callers pick the right label based on the ratio.
 * `null` means the ratio is not meaningful - either side has zero
 * non-blank lines.
 */
export function compressionRatio(tsLines: number, swiftLines: number): string | null {
  if (tsLines === 0 || swiftLines === 0) return null;
  return `${(swiftLines / tsLines).toFixed(2)}x`;
}

interface UnifiedOutput {
  outputPath: string;
  swiftCode: string;
  diagnostics: Diagnostic[];
  irName: string;
  infoPlistFragment?: string;
  entitlementsFragment?: string;
  hasPerformBody?: boolean;
}

/**
 * Flatten the surface-specific result shape into a single output
 * view so downstream CLI formatting doesn't have to fork on surface.
 */
function unifyOutput(result: AnyCompileResult): UnifiedOutput | null {
  if (!result.success || !result.output) return null;

  if (result.surface === "intent") {
    return {
      outputPath: result.output.outputPath,
      swiftCode: result.output.swiftCode,
      diagnostics: result.output.diagnostics,
      irName: result.output.ir.name,
      infoPlistFragment: result.output.infoPlistFragment,
      entitlementsFragment: result.output.entitlementsFragment,
      hasPerformBody: result.output.ir.hasPerformBody,
    };
  }

  if (result.surface === "extension") {
    return {
      outputPath: result.output.outputPath,
      swiftCode: result.output.swiftCode,
      diagnostics: result.output.diagnostics,
      irName: result.output.ir.name,
      infoPlistFragment: result.output.infoPlistFragment,
    };
  }

  return {
    outputPath: result.output.outputPath,
    swiftCode: result.output.swiftCode,
    diagnostics: result.output.diagnostics,
    irName: result.output.ir.name,
  };
}

function printDiagnostics(
  diagnostics: Diagnostic[],
  color: boolean,
  sources?: ReadonlyMap<string, string>
): void {
  for (const d of diagnostics) {
    console.error(renderDiagnostic(d, { color, sources }));
    console.error();
  }
}

export function registerCompile(program: Command) {
  program
    .command("compile")
    .description(
      "Compile a TypeScript surface (intent, entity, view, widget, or app) into Swift"
    )
    .argument("<file>", "Path to the TypeScript surface definition")
    .option("-o, --out <dir>", "Output directory for generated Swift", ".")
    .option("--no-validate", "Skip validation of generated Swift")
    .option("--stdout", "Print generated Swift to stdout instead of writing a file")
    .option("--json", "Output result as JSON (machine-readable)")
    .option(
      "--emit-info-plist",
      "Emit a <Name>.plist.fragment.xml with NSAppIntentsDomains next to the Swift file (intents only)"
    )
    .option(
      "--emit-entitlements",
      "Emit a <Name>.entitlements.fragment.xml next to the Swift file (intents only)"
    )
    .option(
      "--sandbox",
      "Run stage 4 validation: swift build in an SPM sandbox (macOS only)"
    )
    .option(
      "--format",
      "Pipe generated Swift through swift-format with the Axint house style (macOS/Linux if swift-format is on $PATH)"
    )
    .option(
      "--strict-format",
      "Fail the build if swift-format is missing or errors (implies --format)"
    )
    .option(
      "--from-ir",
      "Treat <file> as intent IR JSON (from Python SDK or any language) instead of TypeScript. Use - to read from stdin."
    )
    .option(
      "--no-fix-packet",
      "Skip writing the local Fix Packet under .axint/fix/latest.{json,md}"
    )
    .option(
      "--fix-packet-dir <dir>",
      "Directory for the emitted Fix Packet artifacts",
      ".axint/fix"
    )
    .option(
      "--verbose",
      "Print Fix Packet paths and sign-in tips even when compilation succeeds"
    )
    .action(
      async (
        file: string,
        options: {
          out: string;
          validate: boolean;
          stdout: boolean;
          json: boolean;
          emitInfoPlist: boolean;
          emitEntitlements: boolean;
          sandbox: boolean;
          format: boolean;
          strictFormat: boolean;
          fromIr: boolean;
          fixPacket: boolean;
          fixPacketDir: string;
          verbose?: boolean;
        }
      ) => {
        const filePath = resolve(file);
        const color = shouldUseColor();
        const paint = (ansi: string, text: string) =>
          color ? `${ansi}${text}\x1b[0m` : text;

        try {
          let surface:
            | "intent"
            | "entity"
            | "view"
            | "widget"
            | "app"
            | "liveActivity"
            | "appEnum"
            | "appShortcut"
            | "extension";
          let output: UnifiedOutput | null;
          let diagnostics: Diagnostic[];
          let success: boolean;
          let inputSource: string | undefined;
          let inputFilePath: string | undefined;
          const language: "typescript" | "json" = options.fromIr ? "json" : "typescript";

          if (options.fromIr) {
            let irRaw: string;
            if (file === "-") {
              const chunks: Buffer[] = [];
              for await (const chunk of process.stdin) {
                chunks.push(chunk as Buffer);
              }
              irRaw = Buffer.concat(chunks).toString("utf-8");
            } else {
              irRaw = readFileSync(filePath, "utf-8");
            }
            inputSource = irRaw;
            inputFilePath = file === "-" ? undefined : filePath;

            let parsed: unknown;
            try {
              parsed = JSON.parse(irRaw);
            } catch {
              console.error(`${paint("\x1b[31m", "error:")} Invalid JSON in ${file}`);
              process.exit(1);
            }

            const irData = Array.isArray(parsed)
              ? (parsed[0] as Record<string, unknown>)
              : (parsed as Record<string, unknown>);
            if (!irData || typeof irData !== "object") {
              console.error(
                `${paint("\x1b[31m", "error:")} Expected an IR object or array in ${file}`
              );
              process.exit(1);
            }

            const ir = irFromJSON(irData);
            const result = compileFromIR(ir, {
              outDir: options.out,
              validate: options.validate,
              emitInfoPlist: options.emitInfoPlist,
              emitEntitlements: options.emitEntitlements,
            });
            surface = "intent";
            diagnostics = result.diagnostics;
            success = result.success;
            output =
              result.success && result.output
                ? {
                    outputPath: result.output.outputPath,
                    swiftCode: result.output.swiftCode,
                    diagnostics: result.output.diagnostics,
                    irName: result.output.ir.name,
                    infoPlistFragment: result.output.infoPlistFragment,
                    entitlementsFragment: result.output.entitlementsFragment,
                  }
                : null;
          } else {
            try {
              inputSource = readFileSync(filePath, "utf-8");
              inputFilePath = filePath;
            } catch {
              inputSource = undefined;
              inputFilePath = filePath;
            }
            const result = compileAnyFile(filePath, {
              outDir: options.out,
              validate: options.validate,
              emitInfoPlist: options.emitInfoPlist,
              emitEntitlements: options.emitEntitlements,
            });
            surface = result.surface;
            diagnostics = result.diagnostics;
            success = result.success;
            output = unifyOutput(result);

            if (
              result.surface !== "intent" &&
              (options.emitInfoPlist || options.emitEntitlements)
            ) {
              console.error(
                `${paint("\x1b[33m", "warning:")} --emit-info-plist and --emit-entitlements apply to intents only; ignored for ${result.surface}.`
              );
            }
          }

          let repairArtifacts:
            | ReturnType<typeof tryEmitRepairArtifacts>["artifacts"]
            | null = null;
          if (
            options.fixPacket &&
            surface !== "liveActivity" &&
            surface !== "appEnum" &&
            surface !== "appShortcut" &&
            surface !== "extension" &&
            surface !== "entity"
          ) {
            const resolvedOutputPath =
              success && output && !options.stdout
                ? resolve(output.outputPath)
                : undefined;
            const repairResult = tryEmitRepairArtifacts(
              {
                success,
                surface,
                diagnostics,
                source: inputSource,
                fileName:
                  file === "-"
                    ? "stdin.ir.json"
                    : inputFilePath
                      ? basename(inputFilePath)
                      : basename(filePath),
                filePath: inputFilePath,
                language,
                outputPath: resolvedOutputPath,
                infoPlistPath:
                  success &&
                  output &&
                  !options.stdout &&
                  surface === "intent" &&
                  options.emitInfoPlist &&
                  output.infoPlistFragment
                    ? resolve(output.outputPath).replace(
                        /\.swift$/,
                        ".plist.fragment.xml"
                      )
                    : undefined,
                entitlementsPath:
                  success &&
                  output &&
                  !options.stdout &&
                  surface === "intent" &&
                  options.emitEntitlements &&
                  output.entitlementsFragment
                    ? resolve(output.outputPath).replace(
                        /\.swift$/,
                        ".entitlements.fragment.xml"
                      )
                    : undefined,
                packetDir: options.fixPacketDir,
                command: "compile",
              },
              process.cwd()
            );
            repairArtifacts = repairResult.artifacts;
            if (repairResult.error) {
              console.error(
                `${paint("\x1b[33m", "warning:")} Fix Packet skipped — ${repairResult.error.message}`
              );
            }
          }

          if (options.json) {
            console.log(
              JSON.stringify(
                {
                  success,
                  surface,
                  swift: output?.swiftCode ?? null,
                  outputPath: output?.outputPath ?? null,
                  infoPlistFragment: output?.infoPlistFragment ?? null,
                  entitlementsFragment: output?.entitlementsFragment ?? null,
                  diagnostics: diagnostics.map((d) => ({
                    code: d.code,
                    severity: d.severity,
                    message: d.message,
                    file: d.file,
                    line: d.line,
                    column: d.column,
                    suggestion: d.suggestion,
                  })),
                  fixPacketPath: repairArtifacts?.packet.jsonPath ?? null,
                  checkSummaryPath: repairArtifacts?.check.jsonPath ?? null,
                },
                null,
                2
              )
            );
            if (!success) process.exit(1);
            return;
          }

          const sources =
            inputSource !== undefined && inputFilePath !== undefined
              ? new Map([[inputFilePath, inputSource]])
              : undefined;
          printDiagnostics(diagnostics, color, sources);

          if (!success || !output) {
            if (repairArtifacts) {
              printRepairArtifactLines(repairArtifacts, console.error, { color });
            }
            const errorCount = diagnostics.filter((d) => d.severity === "error").length;
            console.error(
              paint("\x1b[31m", `Compilation failed with ${errorCount} error(s)`)
            );
            process.exit(1);
          }

          if (options.format || options.strictFormat) {
            try {
              const { formatSwift } = await import("../core/format.js");
              const fmt = await formatSwift(output.swiftCode, {
                strict: options.strictFormat,
              });
              if (fmt.ran) {
                output.swiftCode = fmt.formatted;
              } else {
                console.error(
                  `${paint("\x1b[33m", "warning:")} swift-format skipped — ${fmt.reason}`
                );
              }
            } catch (fmtErr: unknown) {
              if (options.strictFormat) {
                console.error(
                  `${paint("\x1b[31m", "error:")} ${(fmtErr as Error).message}`
                );
                process.exit(1);
              }
              console.error(
                `${paint("\x1b[33m", "warning:")} swift-format skipped — ${(fmtErr as Error).message}`
              );
            }
          }

          // The TS perform body never crosses into Swift — saying so up
          // front beats the user discovering a TODO stub in Xcode later.
          if (surface === "intent" && output.hasPerformBody) {
            console.error(
              "note: perform body is not translated yet — Swift stub emitted (implement perform() in the generated file)"
            );
          }

          if (options.stdout) {
            console.log(output.swiftCode);
          } else {
            const outPath = resolve(output.outputPath);
            mkdirSync(dirname(outPath), { recursive: true });
            writeFileSync(outPath, output.swiftCode, "utf-8");
            console.log(
              `${paint("\x1b[32m", "✓")} Compiled ${surface} ${output.irName} → ${outPath}`
            );

            // Show TS-to-Swift compression so authors can eyeball whether
            // a definition expanded the way they expected. Skipped for
            // --from-ir since "TS lines" isn't meaningful there.
            if (!options.fromIr && inputSource) {
              try {
                const tsLines = countNonBlankLines(inputSource);
                const swiftLines = countNonBlankLines(output.swiftCode);
                const ratio = compressionRatio(tsLines, swiftLines);
                if (ratio !== null) {
                  const label = swiftLines > tsLines ? "Expansion" : "Compression";
                  console.log(
                    `${paint("\x1b[36m", "→")} ${label}: ${tsLines} TS → ${swiftLines} Swift (${ratio})`
                  );
                }
              } catch {
                // Non-fatal: we just skip the ratio if the TS source
                // can't be re-read for any reason.
              }
            }

            if (
              surface === "intent" &&
              options.emitInfoPlist &&
              output.infoPlistFragment
            ) {
              const plistPath = outPath.replace(/\.swift$/, ".plist.fragment.xml");
              writeFileSync(plistPath, output.infoPlistFragment, "utf-8");
              console.log(`${paint("\x1b[32m", "✓")} Info.plist fragment → ${plistPath}`);
            }

            if (
              surface === "intent" &&
              options.emitEntitlements &&
              output.entitlementsFragment
            ) {
              const entPath = outPath.replace(/\.swift$/, ".entitlements.fragment.xml");
              writeFileSync(entPath, output.entitlementsFragment, "utf-8");
              console.log(`${paint("\x1b[32m", "✓")} Entitlements fragment → ${entPath}`);
            }

            // Extensions always ship with an NSExtension Info.plist fragment —
            // Xcode needs it to wire the bundle to an extension point, so we
            // emit it unconditionally alongside the Swift.
            if (surface === "extension" && output.infoPlistFragment) {
              const plistPath = outPath.replace(/\.swift$/, ".plist.fragment.xml");
              writeFileSync(plistPath, output.infoPlistFragment, "utf-8");
              console.log(`${paint("\x1b[32m", "✓")} Info.plist fragment → ${plistPath}`);
            }

            if (repairArtifacts && options.verbose) {
              printRepairArtifactLines(repairArtifacts, console.log, { color });
            }
          }

          if (options.sandbox && !options.stdout) {
            try {
              const { sandboxCompile } = await import("../core/sandbox.js");
              console.log();
              console.log(`${paint("\x1b[36m", "→")} Stage 4: SPM sandbox compile...`);
              const sandboxResult = await sandboxCompile(output.swiftCode, {
                intentName: output.irName,
              });
              if (sandboxResult.ok) {
                console.log(
                  `${paint("\x1b[32m", "✓")} Swift builds cleanly (${sandboxResult.durationMs}ms in ${sandboxResult.sandboxPath})`
                );
              } else {
                console.error(
                  `${paint("\x1b[31m", "✗")} Sandbox compile failed:\n${sandboxResult.stderr}`
                );
                process.exit(1);
              }
            } catch (sbErr: unknown) {
              console.error(
                `${paint("\x1b[33m", "warning:")} sandbox compile skipped — ${(sbErr as Error).message}`
              );
            }
          }

          const warnings = diagnostics.filter((d) => d.severity === "warning").length;
          if (warnings > 0) {
            console.log(`  ${warnings} warning(s)`);
          }
        } catch (err: unknown) {
          if (
            err &&
            typeof err === "object" &&
            "format" in err &&
            typeof (err as Record<string, unknown>).format === "function"
          ) {
            console.error((err as { format: () => string }).format());
          } else {
            console.error(`${paint("\x1b[31m", "error:")} ${err}`);
          }
          process.exit(1);
        }
      }
    );
}
