import type { Writable } from "node:stream";
import { z } from "zod";
import { canonicalize } from "../../schema/src/canonical.js";
import type { ClassifiedRun } from "../../core/src/classify/classifier.js";
import { coverageOutcomeSchema } from "../../schema/src/records/coverage.js";
import { artifactRefSchema, findingSchema, type Finding } from "../../schema/src/records/finding.js";
import { harnessOutcomeSchema } from "../../schema/src/records/harness.js";

type ArtifactRef = z.infer<typeof artifactRefSchema>;
import {
  computeExitStatus,
  type ExitStatus,
  type RunConditions,
} from "./exit-status.js";

export const CLI_RESULT_VERSION = "cli-result/v1" as const;

const runConditionsSchema = z
  .object({
    configError: z.boolean(),
    reviewConflict: z.boolean(),
    blockingViolation: z.boolean(),
    harnessFailure: z.boolean(),
    coverageIncomplete: z.boolean(),
  })
  .strict();

const scopesSchema = z
  .object({
    requested: z.array(z.string()),
    defaultScopeApplied: z.boolean(),
    resolved: z.array(z.string()),
  })
  .strict();

const environmentSchema = z
  .object({
    platform: z.string(),
    node: z.string(),
    seed: z.string(),
  })
  .strict();

const countsSchema = z
  .object({
    byLane: z
      .object({
        blocking: z.number(),
        advisory: z.number(),
        accepted: z.number(),
        resolved: z.number(),
      })
      .strict(),
    byClass: z.record(z.string(), z.number()),
    journeys: z.object({ selected: z.number().int().nonnegative(), exercised: z.number().int().nonnegative() }).strict(),
  })
  .strict();

export const hookMetadataSchema = z
  .object({
    mode: z.literal("advisory"),
    impactKnown: z.boolean(),
    requiredCommand: z.string().nullable(),
    changedPaths: z.array(z.string()),
    selectedSurfaceIds: z.array(z.string()),
  })
  .strict();

export const ciSummarySchema = z
  .object({
    lane: z.enum(["focused", "full", "deep"]),
    targets: z.object({ selected: z.number().int().nonnegative(), exercised: z.number().int().nonnegative() }).strict(),
    preconditionFailures: z.array(coverageOutcomeSchema),
    inertPerturbations: z.array(coverageOutcomeSchema),
    unsupportedCapabilities: z.array(coverageOutcomeSchema),
    closure: z.object({ runtime: z.array(coverageOutcomeSchema), artifacts: z.array(coverageOutcomeSchema), bindings: z.array(coverageOutcomeSchema) }).strict(),
    harnessOutcomesByPhase: z.record(z.string(), z.array(harnessOutcomeSchema)),
    coverageOutcomes: z.array(coverageOutcomeSchema),
    productDefects: z.array(findingSchema),
  })
  .strict();

export const machineResultSchema = z
  .object({
    schemaVersion: z.literal(CLI_RESULT_VERSION),
    runId: z.string(),
    exitStatus: z.union([
      z.literal(0),
      z.literal(1),
      z.literal(2),
      z.literal(3),
      z.literal(4),
      z.literal(5),
    ]),
    conditions: runConditionsSchema,
    scopes: scopesSchema,
    findings: z.array(findingSchema),
    coverageOutcomes: z.array(coverageOutcomeSchema),
    harnessOutcomes: z.array(harnessOutcomeSchema),
    counts: countsSchema,
    artifactRefs: z.array(artifactRefSchema),
    timings: z.record(z.string(), z.number()),
    environment: environmentSchema,
    hook: hookMetadataSchema.optional(),
    ci: ciSummarySchema.optional(),
  })
  .strict();

export type MachineResult = z.infer<typeof machineResultSchema>;
export type HookMetadata = z.infer<typeof hookMetadataSchema>;
export type CiSummary = z.infer<typeof ciSummarySchema>;

export type BuildMachineResultInput = {
  runId: string;
  requestedScopes: string[];
  defaultScopeApplied: boolean;
  run: ClassifiedRun;
  conditions: RunConditions;
  timings: Record<string, number>;
  environment: {
    platform: string;
    node: string;
    seed: string;
  };
  exitStatusOverride?: ExitStatus;
  hook?: HookMetadata;
  ci?: CiSummary;
  journeyCounts?: { selected: number; exercised: number };
};

export class MachineOutputError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "MachineOutputError";
  }
}

const LANE_ORDER: Record<Finding["lane"], number> = {
  blocking: 0,
  advisory: 1,
  accepted: 2,
  resolved: 3,
};

const SEVERITY_ORDER: Record<Finding["severity"], number> = {
  critical: 0,
  high: 1,
  medium: 2,
  low: 3,
  info: 4,
};

function compareUnicodeScalars(left: string, right: string): number {
  let leftIndex = 0;
  let rightIndex = 0;

  while (leftIndex < left.length && rightIndex < right.length) {
    const leftCode = left.codePointAt(leftIndex);
    const rightCode = right.codePointAt(rightIndex);
    if (leftCode === undefined || rightCode === undefined) {
      break;
    }
    if (leftCode !== rightCode) {
      return leftCode < rightCode ? -1 : 1;
    }
    leftIndex += leftCode > 0xffff ? 2 : 1;
    rightIndex += rightCode > 0xffff ? 2 : 1;
  }

  return left.length - right.length;
}

function compareFindings(left: Finding, right: Finding): number {
  const laneDelta = LANE_ORDER[left.lane] - LANE_ORDER[right.lane];
  if (laneDelta !== 0) {
    return laneDelta;
  }

  const severityDelta = SEVERITY_ORDER[left.severity] - SEVERITY_ORDER[right.severity];
  if (severityDelta !== 0) {
    return severityDelta;
  }

  const classDelta = compareUnicodeScalars(left.class, right.class);
  if (classDelta !== 0) {
    return classDelta;
  }

  const targetDelta = compareUnicodeScalars(left.target.canonical, right.target.canonical);
  if (targetDelta !== 0) {
    return targetDelta;
  }

  const contextDelta = compareUnicodeScalars(
    canonicalize(left.context),
    canonicalize(right.context),
  );
  if (contextDelta !== 0) {
    return contextDelta;
  }

  return compareUnicodeScalars(left.id, right.id);
}

export function sortFindings(findings: Finding[]): Finding[] {
  return [...findings].sort(compareFindings);
}

function sortById<T extends { id: string }>(records: T[]): T[] {
  return [...records].sort((left, right) => compareUnicodeScalars(left.id, right.id));
}

function resolvedScopes(requestedScopes: string[], defaultScopeApplied: boolean): string[] {
  if (defaultScopeApplied && requestedScopes.length === 0) {
    return ["universal"];
  }
  return [...requestedScopes];
}

function countFindingsByLane(findings: Finding[]): MachineResult["counts"]["byLane"] {
  const counts: MachineResult["counts"]["byLane"] = {
    blocking: 0,
    advisory: 0,
    accepted: 0,
    resolved: 0,
  };

  for (const finding of findings) {
    counts[finding.lane] += 1;
  }

  return counts;
}

function countFindingsByClass(findings: Finding[]): Record<string, number> {
  const counts: Record<string, number> = {};
  for (const finding of findings) {
    counts[finding.class] = (counts[finding.class] ?? 0) + 1;
  }
  return counts;
}

function artifactKey(artifact: ArtifactRef): string {
  return `${artifact.relativePath}\0${artifact.contentHash}`;
}

function collectArtifactRefs(run: ClassifiedRun): ArtifactRef[] {
  const artifacts = new Map<string, ArtifactRef>();

  for (const finding of run.findings) {
    for (const artifact of finding.artifacts) {
      artifacts.set(artifactKey(artifact), artifact);
    }
  }

  for (const outcome of run.harnessOutcomes) {
    for (const artifact of outcome.artifactRefs) {
      artifacts.set(artifactKey(artifact), artifact);
    }
  }

  return [...artifacts.values()].sort((left, right) =>
    compareUnicodeScalars(artifactKey(left), artifactKey(right)),
  );
}

function assertDisjointOutcomeIds(run: ClassifiedRun): void {
  const ids = new Set<string>();
  const findingsById = new Map<string, ClassifiedRun["findings"][number]>();

  for (const finding of run.findings) {
    if (ids.has(finding.id)) {
      const first = findingsById.get(finding.id);
      throw new MachineOutputError(
        `duplicate outcome id: ${finding.id}` +
          (first === undefined
            ? ""
            : ` (${first.detector.id} ${first.class} ${first.target.canonical}` +
              ` vs ${finding.detector.id} ${finding.class} ${finding.target.canonical})`),
      );
    }
    ids.add(finding.id);
    findingsById.set(finding.id, finding);
  }

  for (const outcome of run.coverageOutcomes) {
    if (ids.has(outcome.id)) {
      throw new MachineOutputError(`duplicate outcome id: ${outcome.id}`);
    }
    ids.add(outcome.id);
  }

  for (const outcome of run.harnessOutcomes) {
    if (ids.has(outcome.id)) {
      throw new MachineOutputError(`duplicate outcome id: ${outcome.id}`);
    }
    ids.add(outcome.id);
  }
}

export function buildMachineResult(input: BuildMachineResultInput): MachineResult {
  assertDisjointOutcomeIds(input.run);

  const findings = sortFindings(input.run.findings);
  const exitStatus: ExitStatus =
    input.exitStatusOverride ?? computeExitStatus(input.conditions);

  const result: MachineResult = {
    schemaVersion: CLI_RESULT_VERSION,
    runId: input.runId,
    exitStatus,
    conditions: input.conditions,
    scopes: {
      requested: [...input.requestedScopes],
      defaultScopeApplied: input.defaultScopeApplied,
      resolved: resolvedScopes(input.requestedScopes, input.defaultScopeApplied),
    },
    findings,
    coverageOutcomes: sortById(input.run.coverageOutcomes),
    harnessOutcomes: sortById(input.run.harnessOutcomes),
    counts: {
      byLane: countFindingsByLane(findings),
      byClass: countFindingsByClass(findings),
      journeys: input.journeyCounts ?? { selected: 0, exercised: 0 },
    },
    artifactRefs: collectArtifactRefs(input.run),
    timings: { ...input.timings },
    environment: { ...input.environment },
    ...(input.hook === undefined ? {} : { hook: input.hook }),
    ...(input.ci === undefined ? {} : { ci: input.ci }),
  };

  return parseMachineResult(result);
}

export function parseMachineResult(value: unknown): MachineResult {
  const parsed = machineResultSchema.safeParse(value);
  if (!parsed.success) {
    const issue = parsed.error.issues[0];
    const path = issue?.path.join(".") ?? "$";
    const message = issue?.message ?? "validation failed";
    throw new MachineOutputError(`invalid machine result at ${path}: ${message}`);
  }
  return parsed.data;
}

export function emitMachineResult(result: MachineResult, stdout: Writable): void {
  const validated = parseMachineResult(result);
  stdout.write(`${JSON.stringify(validated)}\n`);
}

export function writeMachineLog(line: string, stderr: Writable): void {
  stderr.write(`${line}\n`);
}
