import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
import { atomicWrite } from "./atomic-write";

export const RoutingProviderSchema = z.enum(["codex", "claude"]);
export type RoutingProvider = z.infer<typeof RoutingProviderSchema>;

const AccountCapsSchema = z.record(z.string().min(1), z.object({
  "5h": z.number().int().min(1).max(100).optional(),
  "7d": z.number().int().min(1).max(100).optional(),
}).strict());

export const RoutingRulesSchema = z.object({
  version: z.literal("routing/v2"),
  projects: z.record(z.string().min(1), z.string().min(1)),
  default: z.string().min(1),
  fallback_chain: z.array(z.string().min(1)),
  fallback_trigger: z.enum(["broken_only", "broken_or_quota_exhausted"]),
  missing_health_is_available: z.boolean(),
  quota_exhausted_threshold_pct: z.number().int().min(1).max(100),
  account_caps: AccountCapsSchema,
}).strict();
export type RoutingRules = z.infer<typeof RoutingRulesSchema>;

export function defaultRoutingRuntimeDir(): string {
  return process.env.OVERDECK_SYSTRAY_RUNTIME_DIR
    ?? join(homedir(), ".local", "state", "overdeck", "systray", "runtime");
}

function providerPaths(runtimeDir: string, provider: RoutingProvider) {
  return provider === "codex"
    ? { rules: join(runtimeDir, "routing_rules.json"), registry: join(runtimeDir, "accounts.json") }
    : { rules: join(runtimeDir, "claude_routing_rules.json"), registry: join(runtimeDir, "claude_accounts.json") };
}

export interface RegisteredAccount { slug: string; label: string }

const AccountSlugSchema = z.string().trim().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/);

export async function readProviderAccounts(runtimeDir: string, provider: RoutingProvider): Promise<RegisteredAccount[]> {
  const raw: unknown = JSON.parse(await readFile(providerPaths(runtimeDir, provider).registry, "utf8"));
  const parsed = z.object({
    accounts: z.array(z.object({ slug: AccountSlugSchema, alias: z.string().trim().min(1).optional() }).passthrough()).min(1),
  }).passthrough().parse(raw);
  const seen = new Set<string>();
  return parsed.accounts.map((account) => {
    if (seen.has(account.slug)) throw new Error(`duplicate ${provider} account slug: ${account.slug}`);
    seen.add(account.slug);
    return { slug: account.slug, label: account.alias ?? account.slug };
  }).sort((a, b) => a.slug.localeCompare(b.slug));
}

export async function validateRoutingRules(
  runtimeDir: string,
  provider: RoutingProvider,
  input: unknown,
): Promise<RoutingRules> {
  const rules = RoutingRulesSchema.parse(input);
  const known = new Set((await readProviderAccounts(runtimeDir, provider)).map((account) => account.slug));
  const referenced = new Set([
    rules.default,
    ...Object.values(rules.projects),
    ...rules.fallback_chain,
    ...Object.keys(rules.account_caps),
  ]);
  for (const slug of referenced) {
    if (!known.has(slug)) throw new Error(`unknown ${provider} account slug: ${slug}`);
  }
  return rules;
}

export async function readRoutingConfig(runtimeDir: string, provider: RoutingProvider): Promise<{
  provider: RoutingProvider;
  accounts: string[];
  rules: RoutingRules;
}> {
  const paths = providerPaths(runtimeDir, provider);
  const [rawRules, accounts] = await Promise.all([
    readFile(paths.rules, "utf8").then((raw) => JSON.parse(raw) as unknown),
    readProviderAccounts(runtimeDir, provider).then((items) => new Set(items.map((item) => item.slug))),
  ]);
  return { provider, accounts: [...accounts].sort(), rules: RoutingRulesSchema.parse(rawRules) };
}

export async function atomicWriteRoutingConfig(
  runtimeDir: string,
  provider: RoutingProvider,
  input: unknown,
): Promise<RoutingRules> {
  const rules = await validateRoutingRules(runtimeDir, provider, input);
  const target = providerPaths(runtimeDir, provider).rules;
  // A directory fsync failure is post-rename: the new rules are committed even
  // though their durability is indeterminate, so never retry over that state.
  await atomicWrite(target, `${JSON.stringify(rules, null, 2)}\n`, {
    temporaryPrefix: `${provider}-routing`,
  });
  return rules;
}
