import { createHash } from "node:crypto";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";

export const FEATURE_STATES = ["OFF", "INTERNAL", "CANARY", "ON", "RETIRED"] as const;
export type FeatureState = (typeof FEATURE_STATES)[number];
/** The only Git identity accepted by delivery state, evidence, and evaluation. */
export const ExactGitShaSchema = z.string().regex(/^[0-9a-f]{40}$/, "must be an exact lowercase 40-hex Git SHA");
export const ExactTreeShaSchema = ExactGitShaSchema;
export const Sha256DigestSchema = z.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase SHA-256 digest");

export const ActivationPolicySchema = z.enum(["dark-only", "auto-after-receipts", "product-decision"]);
const DateSchema = z.string().refine((value) => {
  const parsed = new Date(`${value}T00:00:00.000Z`);
  return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === value;
}, "must be an ISO calendar date");

export const FeatureDefinitionSchema = z.object({
  id: z.string().regex(/^[a-z][a-z0-9-]*$/, "must be a stable lowercase ID"),
  changeClass: z.literal("behavior-change"), owner: z.string().min(1), fallback: z.string().min(1),
  defaultState: z.literal("OFF"), readinessChecks: z.array(z.string().min(1)).min(1),
  observability: z.array(z.string().min(1)).min(1), reviewDate: DateSchema,
  cleanupTask: z.string().regex(/^#[1-9][0-9]*$/), rollbackWindowDays: z.number().int().positive(), removalCondition: z.string().min(1),
  introduced: z.object({ task: z.string().regex(/^#[1-9][0-9]*$/), sha: ExactGitShaSchema }).strict(),
  activationPolicy: ActivationPolicySchema,
}).strict();
export type FeatureDefinition = z.infer<typeof FeatureDefinitionSchema>;
export interface LoadedFeatureDefinition { definition: FeatureDefinition; digest: string; path: string; }
export type RuntimeDefinitionResolution =
  | { available: true; definition: LoadedFeatureDefinition }
  | { available: false; reason: "definition-unavailable" | "invalid-definition" | "missing-definition" | "expired-definition"; detail?: { path?: string; code: string } };
export interface RuntimeFeatureRegistry { available: boolean; definitions: Map<string, LoadedFeatureDefinition>; failure?: RuntimeDefinitionResolution; resolve(id: string): RuntimeDefinitionResolution; }

export function definitionDigest(definition: FeatureDefinition): string { return createHash("sha256").update(JSON.stringify(definition)).digest("hex"); }
export function loadFeatureDefinitions(directory: string): Map<string, LoadedFeatureDefinition> {
  const definitions = new Map<string, LoadedFeatureDefinition>();
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
    if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
    const path = join(directory, entry.name);
    let raw: unknown;
    try { raw = JSON.parse(readFileSync(path, "utf8")); }
    catch (error) { throw new Error(`invalid feature definition ${path}: ${(error as Error).message}`); }
    let definition: FeatureDefinition;
    try { definition = FeatureDefinitionSchema.parse(raw); }
    catch (error) { throw new Error(`invalid feature definition ${path}: ${(error as Error).message}`); }
    if (definitions.has(definition.id)) throw new Error(`duplicate feature definition ID: ${definition.id}`);
    definitions.set(definition.id, { definition, digest: definitionDigest(definition), path });
  }
  return definitions;
}
/** Runtime loading is deliberately separate from strict CI validation: one bad file disables all. */
export function loadRuntimeFeatureDefinitions(directory: string): RuntimeFeatureRegistry {
  try {
    const definitions = loadFeatureDefinitions(directory);
    return { available: true, definitions, resolve: (id) => {
      const definition = definitions.get(id);
      return definition ? { available: true, definition } : { available: false, reason: "missing-definition", detail: { code: "not-found" } };
    } };
  } catch (error) {
    const message = error instanceof Error ? error.message : "unknown";
    const path = message.match(/(?:definition )([^:]+):/)?.[1];
    const reason = message.startsWith("invalid feature definition") || message.includes("duplicate feature definition") || message.includes("Zod") ? "invalid-definition" : "definition-unavailable";
    const failure: RuntimeDefinitionResolution = { available: false, reason, detail: { ...(path ? { path } : {}), code: reason } };
    return { available: false, definitions: new Map(), failure, resolve: () => failure };
  }
}
export function resolveRuntimeFeatureDefinition(registry: RuntimeFeatureRegistry, id: string, now: () => number = () => Date.now()): RuntimeDefinitionResolution {
  const resolution = registry.resolve(id);
  if (!resolution.available) return resolution;
  const reviewDeadline = Date.parse(`${resolution.definition.definition.reviewDate}T23:59:59.999Z`);
  return now() > reviewDeadline
    ? { available: false, reason: "expired-definition", detail: { path: resolution.definition.path, code: "review-date-expired" } }
    : resolution;
}
export function loadTrackedRuntimeFeatureDefinitions(): RuntimeFeatureRegistry { const here = fileURLToPath(new URL(".", import.meta.url)); return loadRuntimeFeatureDefinitions(join(here, "../../../delivery/features")); }
export function trackedFeatureDefinitions(): Map<string, LoadedFeatureDefinition> { const here = fileURLToPath(new URL(".", import.meta.url)); return loadFeatureDefinitions(join(here, "../../../delivery/features")); }
