import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { z } from "zod";
import type { JobRecord, TransitionVerb } from "./store";

export const EventSchema = z
  .object({
    ts: z.string().datetime(),
    job: z.string(),
    repo: z.string(),
    host: z.string(),
    snapshot: z.string(),
    attempt: z.number().int().nonnegative(),
    stage: z.string(),
    reason: z.string(),
    rc: z.number().int().nullable(),
    durationSeconds: z.number().nonnegative(),
  })
  .strict()
  .superRefine((event, context) => {
    if (event.job !== "") return;
    if (
      event.repo !== "" ||
      event.snapshot !== "" ||
      event.attempt !== 0 ||
      event.rc !== null ||
      event.durationSeconds !== 0
    ) {
      context.addIssue({
        code: z.ZodIssueCode.custom,
        message: "non-job events require empty job fields, zero attempt/duration, and null rc",
      });
    }
  });

export type ControllerEvent = z.infer<typeof EventSchema>;

export interface StoredEvent {
  revision: number;
  event: ControllerEvent;
}

export type IncidentLifecycle = "incident-opened" | "incident-resolved";

interface EventStore {
  getJob(id: string): JobRecord | null;
  readEvents(afterRevision?: number): StoredEvent[];
}

export function buildTransitionEvent(
  store: Pick<EventStore, "getJob">,
  verb: TransitionVerb,
  args: Record<string, unknown>,
  result: unknown,
  now: () => number,
): ControllerEvent {
  const resultRecord = result && typeof result === "object"
    ? result as Record<string, unknown>
    : {};
  const job = typeof args.jobId === "string" ? store.getJob(args.jobId) : null;
  if (job) {
    return EventSchema.parse({
      ts: new Date(now()).toISOString(),
      job: job.id,
      repo: job.repo,
      host: job.host,
      snapshot: job.snapshot,
      attempt: verb === "job-retry" ? job.attempt + 1 : job.attempt,
      stage: verb === "job-retry" ? "queued" : job.stage,
      reason: verb,
      rc: job.rc,
      durationSeconds: 0,
    });
  }

  const host = typeof args.host === "string"
    ? args.host
    : typeof resultRecord.host === "string"
      ? resultRecord.host
      : "";
  return EventSchema.parse({
    ts: new Date(now()).toISOString(),
    job: "",
    repo: "",
    host,
    snapshot: "",
    attempt: 0,
    stage: verb,
    reason: verb,
    rc: null,
    durationSeconds: 0,
  });
}

export function buildCapabilityEvent(
  host: string,
  command: string,
  reason: "capability-missing" | "capability-restored",
  now: () => number,
): ControllerEvent {
  return EventSchema.parse({
    ts: new Date(now()).toISOString(),
    job: "",
    repo: "",
    host,
    snapshot: "",
    attempt: 0,
    stage: command,
    reason,
    rc: null,
    durationSeconds: 0,
  });
}

export function buildJobReportStartedEvent(input: {
  startedAt: string;
  jobId: string;
  repo: string;
  host: string;
  snapshot: string;
  attempt: number;
}): ControllerEvent {
  return EventSchema.parse({
    ts: input.startedAt,
    job: input.jobId,
    repo: input.repo,
    host: input.host,
    snapshot: input.snapshot,
    attempt: input.attempt,
    stage: "started",
    reason: "report-started",
    rc: null,
    durationSeconds: 0,
  });
}

export function buildJobReportFinishedEvent(input: {
  finishedAt: string;
  startedAt: string;
  jobId: string;
  repo: string;
  host: string;
  snapshot: string;
  attempt: number;
  rc: number;
}): ControllerEvent {
  const durationSeconds = Math.max(
    0,
    (Date.parse(input.finishedAt) - Date.parse(input.startedAt)) / 1000,
  );
  return EventSchema.parse({
    ts: input.finishedAt,
    job: input.jobId,
    repo: input.repo,
    host: input.host,
    snapshot: input.snapshot,
    attempt: input.attempt,
    stage: "finished",
    reason: "report-finished",
    rc: input.rc,
    durationSeconds,
  });
}

export function buildJobReportConflictEvent(input: {
  ts: string;
  jobId: string;
  repo: string;
  host: string;
  snapshot: string;
  attempt: number;
  rc: number | null;
  reportStage: "started" | "finished";
}): ControllerEvent {
  return EventSchema.parse({
    ts: input.ts,
    job: input.jobId,
    repo: input.repo,
    host: input.host,
    snapshot: input.snapshot,
    attempt: input.attempt,
    stage: "report-conflict",
    reason: input.reportStage === "started"
      ? "started-payload-mismatch"
      : "finished-payload-mismatch",
    rc: input.rc,
    durationSeconds: 0,
  });
}

export function buildSpineConfigMissingEvent(observedAt: string): ControllerEvent {
  return EventSchema.parse({
    ts: observedAt,
    job: "",
    repo: "",
    host: "local",
    snapshot: "",
    attempt: 0,
    stage: "config-missing",
    reason: "config-absent",
    rc: null,
    durationSeconds: 0,
  });
}

export function buildSpineConfigInvalidEvent(input: {
  observedAt: string;
  kind: "config-invalid-json" | "config-invalid-shape";
  override: boolean;
}): ControllerEvent {
  return EventSchema.parse({
    ts: input.observedAt,
    job: "",
    repo: "",
    host: "local",
    snapshot: "",
    attempt: 0,
    stage: "config-invalid",
    reason: input.override
      ? `authorized-local-fallback:${input.kind}`
      : input.kind,
    rc: null,
    durationSeconds: 0,
  });
}

export function buildSpineConfigValidEvent(input: {
  observedAt: string;
  disabled: boolean;
  override: boolean;
}): ControllerEvent {
  return EventSchema.parse({
    ts: input.observedAt,
    job: "",
    repo: "",
    host: "local",
    snapshot: "",
    attempt: 0,
    stage: "config-valid",
    reason: `config-valid:${input.disabled ? "disabled" : "enabled"}:${input.override ? "override" : "default"}`,
    rc: null,
    durationSeconds: 0,
  });
}

export function buildIncidentLifecycleEvent(
  key: string,
  lifecycle: IncidentLifecycle,
  host: string,
  now: () => number,
): ControllerEvent {
  return EventSchema.parse({
    ts: new Date(now()).toISOString(),
    job: "",
    repo: "",
    host,
    snapshot: "",
    attempt: 0,
    stage: lifecycle,
    reason: key,
    rc: null,
    durationSeconds: 0,
  });
}

export function projectEventLog(store: Pick<EventStore, "readEvents">, path: string): void {
  const serialized = store.readEvents(0).map(({ event }) => JSON.stringify(event));
  mkdirSync(dirname(path), { recursive: true });
  if (!existsSync(path)) {
    writeFileSync(path, serialized.length === 0 ? "" : `${serialized.join("\n")}\n`, "utf8");
    return;
  }

  const currentText = readFileSync(path, "utf8");
  const current = currentText === ""
    ? []
    : currentText.trimEnd().split("\n");
  const prefixMatches = current.length <= serialized.length &&
    current.every((line, index) => line === serialized[index]);
  if (!prefixMatches) {
    writeFileSync(path, serialized.length === 0 ? "" : `${serialized.join("\n")}\n`, "utf8");
    return;
  }

  const missing = serialized.slice(current.length);
  if (missing.length > 0) {
    appendFileSync(path, `${missing.join("\n")}\n`, "utf8");
  }
}
