/**
 * Zod schemas describing the rpki_query_service API contract.
 *
 * Schemas are intentionally `.passthrough()` so additive backend changes do
 * not break the UI; fields the UI depends on are typed strictly.
 */
import { z } from "zod";

/* ------------------------------------------------------------------ */
/* Records                                                             */
/* ------------------------------------------------------------------ */

export const runCountsSchema = z
  .object({
    publicationPoints: z.number().optional(),
    objects: z.number().optional(),
    freshObjects: z.number().optional(),
    cachedObjects: z.number().optional(),
    rejectedObjects: z.number().optional(),
    freshRejectedObjects: z.number().optional(),
    cachedRejectedObjects: z.number().optional(),
    trustAnchors: z.number().optional(),
    vrps: z.number().optional(),
    aspas: z.number().optional(),
    warnings: z.number().optional(),
  })
  .passthrough();

export const runRecordSchema = z
  .object({
    schemaVersion: z.number().optional(),
    runId: z.string(),
    runSeq: z.number().nullish(),
    runDir: z.string().optional(),
    validationTime: z.string().nullish(),
    syncMode: z.string().nullish(),
    startedAt: z.string().nullish(),
    finishedAt: z.string().nullish(),
    wallMs: z.number().nullish(),
    maxRssKb: z.number().nullish(),
    artifactPaths: z.record(z.string()).optional(),
    counts: runCountsSchema.optional(),
    indexStatus: z.string().optional(),
    indexError: z.string().nullish(),
  })
  .passthrough();

export const repositoryRecordSchema = z
  .object({
    repoId: z.string(),
    uri: z.string(),
    host: z.string(),
    transport: z.string().nullish(),
    publicationPoints: z.number().nullish(),
    objects: z.number().nullish(),
    rejectedObjects: z.number().nullish(),
    downloadBytes: z.number().nullish(),
    syncDurationMsTotal: z.number().nullish(),
    phases: z.record(z.number()).optional(),
    terminalStates: z.record(z.number()).optional(),
  })
  .passthrough();

export const publicationPointRecordSchema = z
  .object({
    ppId: z.string(),
    repoId: z.string(),
    nodeId: z.number().nullish(),
    parentNodeId: z.number().nullish(),
    rsyncBaseUri: z.string().nullish(),
    manifestRsyncUri: z.string().nullish(),
    publicationPointRsyncUri: z.string().nullish(),
    rrdpNotificationUri: z.string().nullish(),
    source: z.string().nullish(),
    repoSyncSource: z.string().nullish(),
    repoSyncPhase: z.string().nullish(),
    repoSyncDurationMs: z.number().nullish(),
    repoSyncError: z.string().nullish(),
    repoTerminalState: z.string().nullish(),
    thisUpdate: z.string().nullish(),
    nextUpdate: z.string().nullish(),
    verifiedAt: z.string().nullish(),
    objects: z.number().nullish(),
    rejectedObjects: z.number().nullish(),
    warnings: z.number().nullish(),
  })
  .passthrough();

export const objectInstanceRecordSchema = z
  .object({
    objectInstanceId: z.string(),
    uri: z.string(),
    uriHash: z.string().optional(),
    sha256: z.string().optional(),
    objectType: z.string(),
    result: z.string().nullish(),
    detailSummary: z.string().nullish(),
    repoId: z.string(),
    ppId: z.string(),
    sourceSection: z.string().nullish(),
    rejected: z.boolean().optional(),
    rejectReason: z.string().nullish(),
    sizeBytes: z.number().nullish(),
    rawAvailable: z.boolean().optional(),
  })
  .passthrough();

export const chainEdgeRecordSchema = z
  .object({
    relation: z.string(),
    fromUri: z.string().nullish(),
    toUri: z.string().nullish(),
    toObjectInstanceId: z.string().nullish(),
    toSha256: z.string().nullish(),
    status: z.string().nullish(),
    evidence: z.unknown().optional(),
  })
  .passthrough();

/** Sparse record returned by /objects/by-uri (index hit, no lazy scan). */
export const objectUriIndexRecordSchema = z
  .object({
    uri: z.string(),
    sha256: z.string(),
    objectInstanceId: z.string(),
    repoId: z.string(),
    ppId: z.string(),
  })
  .passthrough();

export const exportJobRecordSchema = z
  .object({
    jobId: z.string(),
    runId: z.string(),
    scope: z.string(),
    repoId: z.string().nullish(),
    ppId: z.string().nullish(),
    status: z.string(),
    createdAt: z.string().nullish(),
    finishedAt: z.string().nullish(),
    outputPath: z.string().nullish(),
    objectCount: z.number().nullish(),
    bytesWritten: z.number().nullish(),
    error: z.string().nullish(),
  })
  .passthrough();

export const projectionRecordSchema = z
  .object({
    schemaVersion: z.number().optional(),
    sha256: z.string().optional(),
    objectType: z.string().optional(),
    parseStatus: z.string().optional(),
    errorSummary: z.string().nullish(),
    projection: z.record(z.unknown()).nullish(),
  })
  .passthrough();

export const validationIssueSchema = z
  .object({
    severity: z.string().nullish(),
    reasonCode: z.string().nullish(),
    reasonText: z.string().nullish(),
    rfcRefs: z.array(z.string()).optional(),
    evidence: z.unknown().optional(),
  })
  .passthrough();

export const validationStageSchema = z
  .object({
    status: z.string().nullish(),
    stage: z.string().nullish(),
    mode: z.string().nullish(),
    projectionStatus: z.string().nullish(),
    issues: z.array(validationIssueSchema).optional(),
    detailSummary: z.string().nullish(),
    edgesCount: z.number().nullish(),
    note: z.string().nullish(),
  })
  .passthrough();

export const validationSummarySchema = z
  .object({
    finalStatus: z.string().nullish(),
    auditResult: z.string().nullish(),
    detailSummary: z.string().nullish(),
    parsevalidate: validationStageSchema.nullish(),
    chainvalidate: validationStageSchema.nullish(),
  })
  .passthrough();

export const explainRecordSchema = z
  .object({
    finalStatus: z.string().nullish(),
    auditResult: z.string().nullish(),
    authoritative: z.boolean().optional(),
    explainMode: z.string().nullish(),
    parsevalidate: validationStageSchema.nullish(),
    chainvalidate: validationStageSchema.nullish(),
    chainEdges: z.array(chainEdgeRecordSchema).optional(),
  })
  .passthrough();

export const searchResultSchema = z
  .object({
    objects: z.array(objectInstanceRecordSchema),
    repos: z.array(repositoryRecordSchema),
    publicationPoints: z.array(publicationPointRecordSchema),
  })
  .passthrough();

export const vrpRecordSchema = z
  .object({
    runId: z.string(),
    asn: z.number(),
    prefix: z.string(),
    maxLength: z.number(),
  })
  .passthrough();

export const healthSchema = z
  .object({
    status: z.string(),
    service: z.string().optional(),
    version: z.number().optional(),
    latestReadyRun: z.string().nullish(),
  })
  .passthrough();

export const serviceInfoSchema = z
  .object({
    service: z.string(),
    version: z.number(),
  })
  .passthrough();

export const manifestFileEntrySchema = z
  .object({
    fileName: z.string(),
    hashHex: z.string(),
  })
  .passthrough();

export const revokedCertEntrySchema = z
  .object({
    serialNumberHex: z.string().nullish(),
    // Backend serializes CRL serials as a JSON number when they fit, else a string.
    serialNumber: z.union([z.string(), z.number()]).nullish(),
    revocationDate: z.string().nullish(),
  })
  .passthrough();

export const runSummarySchema = z
  .object({
    publicationPoints: z.number().optional(),
    objects: z.number().optional(),
    repos: z.number().optional(),
    vrps: z.number().optional(),
    aspas: z.number().optional(),
  })
  .passthrough();

export const statsMapSchema = z.record(z.number());

/* ------------------------------------------------------------------ */
/* Envelope                                                            */
/* ------------------------------------------------------------------ */

const pageSchema = z.object({
  nextCursor: z.string().nullable(),
  limit: z.number(),
});

const metaSchema = z
  .object({
    runId: z.string().nullish(),
    schemaVersion: z.number().optional(),
  })
  .passthrough();

export function envelopeSchema<T extends z.ZodTypeAny>(dataSchema: T) {
  return z
    .object({
      data: dataSchema,
      page: pageSchema.nullish(),
      meta: metaSchema.optional(),
    })
    .passthrough();
}

/* ------------------------------------------------------------------ */
/* Inferred types                                                      */
/* ------------------------------------------------------------------ */

export type RunCounts = z.infer<typeof runCountsSchema>;
export type RunRecord = z.infer<typeof runRecordSchema>;
export type RepositoryRecord = z.infer<typeof repositoryRecordSchema>;
export type PublicationPointRecord = z.infer<typeof publicationPointRecordSchema>;
export type ObjectInstanceRecord = z.infer<typeof objectInstanceRecordSchema>;
export type ChainEdgeRecord = z.infer<typeof chainEdgeRecordSchema>;
export type ExportJobRecord = z.infer<typeof exportJobRecordSchema>;
export type ProjectionRecord = z.infer<typeof projectionRecordSchema>;
export type ValidationIssue = z.infer<typeof validationIssueSchema>;
export type ValidationStage = z.infer<typeof validationStageSchema>;
export type ValidationSummary = z.infer<typeof validationSummarySchema>;
export type ExplainRecord = z.infer<typeof explainRecordSchema>;
export type SearchResult = z.infer<typeof searchResultSchema>;
export type VrpRecord = z.infer<typeof vrpRecordSchema>;
export type HealthStatus = z.infer<typeof healthSchema>;
export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
export type ManifestFileEntry = z.infer<typeof manifestFileEntrySchema>;
export type RevokedCertEntry = z.infer<typeof revokedCertEntrySchema>;
export type RunSummary = z.infer<typeof runSummarySchema>;

export interface ListResult<T> {
  items: T[];
  nextCursor: string | null;
  runId: string | undefined;
}
