import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";

const DEFAULT_BASE_URL = "http://127.0.0.1:8787";
const DEFAULT_TIMEOUT_MS = 500;

export const MirrorSchema = z
  .string()
  .min(1)
  .refine(
    (value) => !value.includes("/") && !value.includes("\\"),
    "mirror must not contain path separators",
  );

export const JobReportSchema = z
  .object({
    source: z.literal("remote-build"),
    host: z.string().min(1),
    key: z.string().min(1),
    mirror: MirrorSchema,
    repo: z.string().min(1),
    snapshot: z.string().min(1),
    argv: z.array(z.string()).min(1),
    attempt: z.number().int().positive(),
    stage: z.enum(["started", "finished"]),
    rc: z.number().int().optional(),
    startedAt: z.string().datetime(),
    finishedAt: z.string().datetime().optional(),
    timeoutSec: z.number().positive(),
  })
  .strict()
  .superRefine((body, context) => {
    if (body.stage === "finished") {
      if (body.rc === undefined) {
        context.addIssue({
          code: z.ZodIssueCode.custom,
          message: "rc is required when stage is finished",
        });
      }
      if (!body.finishedAt) {
        context.addIssue({
          code: z.ZodIssueCode.custom,
          message: "finishedAt is required when stage is finished",
        });
      }
    }
  });

export const ConfigReportMissingSchema = z
  .object({
    source: z.literal("remote-build"),
    stage: z.literal("missing"),
    configPath: z.string().min(1),
    observedAt: z.string().datetime(),
  })
  .strict();

export const ConfigReportInvalidSchema = z
  .object({
    source: z.literal("remote-build"),
    stage: z.literal("invalid"),
    kind: z.enum(["config-invalid-json", "config-invalid-shape"]),
    detail: z.string(),
    configPath: z.string().min(1),
    preservedPath: z.string().min(1),
    observedAt: z.string().datetime(),
    override: z.boolean(),
  })
  .strict();

export const ConfigReportValidSchema = z
  .object({
    source: z.literal("remote-build"),
    stage: z.literal("valid"),
    configPath: z.string().min(1),
    observedAt: z.string().datetime(),
    disabled: z.boolean(),
    override: z.boolean(),
  })
  .strict();

export const ConfigReportSchema = z.discriminatedUnion("stage", [
  ConfigReportMissingSchema,
  ConfigReportInvalidSchema,
  ConfigReportValidSchema,
]);

export type JobReport = z.infer<typeof JobReportSchema>;
export type ConfigReport = z.infer<typeof ConfigReportSchema>;

export type SpineFetcher = (
  input: string | URL | Request,
  init?: RequestInit,
) => Promise<Response>;

export type ReportClientOptions = {
  baseUrl?: string;
  tokenPath?: string;
  timeoutMs?: number;
  fetcher?: SpineFetcher;
};

function configDir(): string {
  return process.env.OVERDECK_CONFIG_DIR ?? join(homedir(), ".config", "overdeck");
}

function resolveTokenPath(opts?: ReportClientOptions): string {
  return opts?.tokenPath ?? join(configDir(), "token");
}

function readToken(opts?: ReportClientOptions): string | null {
  try {
    const contents = readFileSync(resolveTokenPath(opts), "utf8").trim();
    return contents.length > 0 ? contents : null;
  } catch {
    return null;
  }
}

async function postReport(
  path: string,
  body: unknown,
  opts?: ReportClientOptions,
): Promise<void> {
  if (!process.env.OVERDECK_SPINE_REPORT) {
    return;
  }

  const token = readToken(opts);
  if (!token) {
    return;
  }

  const baseUrl = opts?.baseUrl ?? DEFAULT_BASE_URL;
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const fetcher = opts?.fetcher ?? fetch;

  try {
    const response = await fetcher(`${baseUrl}${path}`, {
      method: "POST",
      headers: {
        authorization: `Bearer ${token}`,
        "content-type": "application/json",
      },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(timeoutMs),
    });
    if (!response.ok) {
      return;
    }
    await response.arrayBuffer();
  } catch {
    // Observation fail-open.
  }
}

export async function reportJob(event: JobReport, opts?: ReportClientOptions): Promise<void> {
  const parsed = JobReportSchema.safeParse(event);
  if (!parsed.success) {
    throw parsed.error;
  }
  await postReport("/jobs/report", parsed.data, opts);
}

export async function reportConfig(event: ConfigReport, opts?: ReportClientOptions): Promise<void> {
  const parsed = ConfigReportSchema.safeParse(event);
  if (!parsed.success) {
    throw parsed.error;
  }
  await postReport("/spine/config/report", parsed.data, opts);
}

export function parseReportInput(raw: unknown):
  | { kind: "job"; event: JobReport }
  | { kind: "config"; event: ConfigReport }
  | { kind: "invalid"; detail: string } {
  if (typeof raw !== "object" || raw === null) {
    return { kind: "invalid", detail: "report body must be an object" };
  }

  const stage = (raw as { stage?: unknown }).stage;
  if (stage === "started" || stage === "finished") {
    const parsed = JobReportSchema.safeParse(raw);
    if (!parsed.success) {
      return { kind: "invalid", detail: parsed.error.message };
    }
    return { kind: "job", event: parsed.data };
  }

  if (stage === "missing" || stage === "invalid" || stage === "valid") {
    const parsed = ConfigReportSchema.safeParse(raw);
    if (!parsed.success) {
      return { kind: "invalid", detail: parsed.error.message };
    }
    return { kind: "config", event: parsed.data };
  }

  return { kind: "invalid", detail: "unknown report stage" };
}
