// report.json and the review API are contract boundaries: a corrupt or hostile payload
// must be rejected here, not cast and rendered. The client is a zero-dependency offline
// bundle (PLAN-010), so validation is structural rather than a bundled schema library.
// Only fields the client consumes are checked; unknown keys pass through, because the
// producing schemas own strictness and mirroring them here would drift.
// Enum members are pinned against the canonical schemas by report-contract.test.ts.

export type Validator<T> = (value: unknown, path: string) => T;

class ContractError extends Error {
  constructor(
    readonly path: string,
    reason: string,
  ) {
    super(`${path}: ${reason}`);
  }
}

function fail(path: string, reason: string): never {
  throw new ContractError(path, reason);
}

export const unknownValue: Validator<unknown> = (value) => value;

export const stringValue: Validator<string> = (value, path) =>
  typeof value === "string" ? value : fail(path, "expected string");

export const numberValue: Validator<number> = (value, path) =>
  typeof value === "number" && Number.isFinite(value) ? value : fail(path, "expected number");

export function literal<T extends string>(expected: T): Validator<T> {
  return (value, path) =>
    value === expected ? expected : fail(path, `expected "${expected}"`);
}

export function enumValue<T extends string>(members: readonly T[]): Validator<T> {
  return (value, path) =>
    typeof value === "string" && (members as readonly string[]).includes(value)
      ? (value as T)
      : fail(path, `expected one of ${members.join(", ")}`);
}

export function arrayOf<T>(item: Validator<T>): Validator<T[]> {
  return (value, path) => {
    if (!Array.isArray(value)) fail(path, "expected array");
    return value.map((entry, index) => item(entry, `${path}[${String(index)}]`));
  };
}

export function recordOf<T>(item: Validator<T>): Validator<Record<string, T>> {
  return (value, path) => {
    if (typeof value !== "object" || value === null || Array.isArray(value)) {
      fail(path, "expected object");
    }
    const out: Record<string, T> = {};
    for (const [key, entry] of Object.entries(value)) {
      out[key] = item(entry, `${path}.${key}`);
    }
    return out;
  };
}

type Shape = Record<string, Validator<unknown>>;
type Optionals = Record<string, Validator<unknown>>;

export function objectOf<T>(required: Shape, optional: Optionals = {}): Validator<T> {
  return (value, path) => {
    if (typeof value !== "object" || value === null || Array.isArray(value)) {
      fail(path, "expected object");
    }
    const source = value as Record<string, unknown>;
    const out: Record<string, unknown> = { ...source };
    for (const [key, validate] of Object.entries(required)) {
      if (!(key in source)) fail(`${path}.${key}`, "required");
      out[key] = validate(source[key], `${path}.${key}`);
    }
    for (const [key, validate] of Object.entries(optional)) {
      if (source[key] !== undefined) {
        out[key] = validate(source[key], `${path}.${key}`);
      }
    }
    return out as T;
  };
}

export const FINDING_LANES = ["blocking", "advisory", "accepted", "resolved"] as const;
export const FINDING_CERTAINTIES = [
  "proven",
  "confirmed-contract",
  "inferred",
  "visual-advisory",
] as const;
export const FINDING_SEVERITIES = ["critical", "high", "medium", "low", "info"] as const;
export const COVERAGE_REASONS = [
  "empty-selection",
  "unproven-precondition",
  "inert-perturbation",
  "failed-known-bad",
  "unsupported-capability",
  "unreachable-target",
  "missing-artifact",
] as const;
export const HARNESS_PHASES = ["setup", "adapter", "detector", "infrastructure"] as const;
export const REVIEW_VERDICTS = ["confirmed_defect", "not_defect"] as const;
export const IMAGE_STATUSES = ["available", "unavailable", "not-applicable"] as const;

const artifactRefValidator = objectOf(
  {
    relativePath: stringValue,
    mediaType: stringValue,
    contentHash: stringValue,
    redactionState: enumValue(["none", "partial", "full"] as const),
    sourceRunId: stringValue,
  },
  {
    dimensions: objectOf({ width: numberValue, height: numberValue }),
  },
);

const reportFindingValidator = objectOf(
  {
    schemaVersion: literal("finding/v1"),
    id: stringValue,
    evidenceFingerprint: stringValue,
    detector: objectOf({ id: stringValue, version: stringValue }),
    class: stringValue,
    lane: enumValue(FINDING_LANES),
    certainty: enumValue(FINDING_CERTAINTIES),
    severity: enumValue(FINDING_SEVERITIES),
    target: objectOf({ kind: stringValue, canonical: stringValue }),
    context: unknownValue,
    summary: stringValue,
    evidence: unknownValue,
    artifacts: arrayOf(artifactRefValidator),
    contractIds: arrayOf(stringValue),
    firstSeenRunId: stringValue,
    lastSeenRunId: stringValue,
    imageStatus: enumValue(IMAGE_STATUSES),
  },
  {
    reopenedFrom: objectOf(
      {
        priorDecisionId: stringValue,
        priorFingerprint: stringValue,
        currentFingerprint: stringValue,
        reviewer: stringValue,
        reviewedAt: stringValue,
      },
      { reason: stringValue, scope: unknownValue },
    ),
  },
);

const coverageOutcomeValidator = objectOf({
  schemaVersion: literal("coverage-outcome/v1"),
  id: stringValue,
  status: literal("coverage-incomplete"),
  scope: unknownValue,
  context: unknownValue,
  reason: enumValue(COVERAGE_REASONS),
  witnessRefs: arrayOf(objectOf({ id: stringValue }, { artifactRef: artifactRefValidator })),
  runId: stringValue,
}, {
  interaction: objectOf({
    gapKind: stringValue,
    control: objectOf({ accessibleName: stringValue, locator: stringValue }),
  }),
});

const reportCellValidator = objectOf({
  id: stringValue,
  route: stringValue,
  url: stringValue,
  role: stringValue,
  locale: stringValue,
  direction: stringValue,
  viewport: objectOf({ width: numberValue, height: numberValue, deviceScaleFactor: numberValue }),
  engine: stringValue,
});

const harnessOutcomeValidator = objectOf({
  schemaVersion: literal("harness-outcome/v1"),
  id: stringValue,
  status: literal("harness-failure"),
  phase: enumValue(HARNESS_PHASES),
  scope: unknownValue,
  cause: objectOf({ code: stringValue, message: stringValue, retryable: unknownValue }),
  artifactRefs: arrayOf(artifactRefValidator),
  runId: stringValue,
});

export const reportDataSchema = objectOf(
  {
    schemaVersion: literal("report/v1"),
    findings: arrayOf(reportFindingValidator),
    coverageOutcomes: arrayOf(coverageOutcomeValidator),
    harnessOutcomes: arrayOf(harnessOutcomeValidator),
    cellIndex: arrayOf(reportCellValidator),
  },
  {
    acceptedFindings: arrayOf(
      objectOf(
        {
          finding: reportFindingValidator,
          decision: objectOf(
            { id: stringValue, reviewer: stringValue, reviewedAt: stringValue },
            { reason: stringValue, scope: unknownValue },
          ),
          status: enumValue(["accepted", "revoked"] as const),
        },
        { revokedAt: stringValue },
      ),
    ),
    staticReview: objectOf({
      projectIdentity: stringValue,
      configHash: stringValue,
      report: objectOf({ id: stringValue, revision: stringValue, findingSetHash: stringValue }),
    }),
  },
);

const stagedDecisionValidator = objectOf(
  {
    findingId: stringValue,
    evidenceFingerprint: stringValue,
    verdict: enumValue(REVIEW_VERDICTS),
    decidedAt: stringValue,
  },
  {
    scope: unknownValue,
    reason: stringValue,
    promotion: objectOf({
      status: literal("staged"),
      requestPath: stringValue,
      contractId: stringValue,
      contractVersion: stringValue,
      stagedAt: stringValue,
    }),
  },
);

export const reviewDraftSchema = objectOf({
  schemaVersion: literal("review-draft/v1"),
  revision: numberValue,
  decisions: arrayOf(stagedDecisionValidator),
  notes: recordOf(stringValue),
  undoStacks: recordOf(
    arrayOf(objectOf({}, { verdict: enumValue(REVIEW_VERDICTS), reason: stringValue, scope: unknownValue })),
  ),
});

export const draftPayloadSchema = objectOf({ draft: reviewDraftSchema });

export const conflictPayloadSchema = objectOf({
  draft: reviewDraftSchema,
  changedDecisions: arrayOf(stagedDecisionValidator),
});

export const reviewDraftPayloadSchema = objectOf({
  draft: reviewDraftSchema,
  promotionPreviews: recordOf(unknownValue),
});

export const reportClientConfigSchema = objectOf<{ apiToken?: string }>({}, { apiToken: stringValue });

export function parseContract<T>(schema: Validator<T>, value: unknown, label: string): T {
  try {
    return schema(value, "$");
  } catch (error) {
    const detail = error instanceof ContractError ? error.message : "unknown mismatch";
    throw new Error(`${label} failed schema validation (${detail})`);
  }
}
