import { createHash } from "node:crypto";
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { parse, stringify } from "yaml";

export * from "./price-calc.ts";
import type { PriceCatalog, ModelPriceRow, PriceTier } from "./price-calc.ts";
import { PRICES_SCHEMA } from "./price-calc.ts";

export function hashPriceCatalog(catalog: Omit<PriceCatalog, "sha256">): string {
  const canonical = {
    schema: catalog.schema,
    date: catalog.date,
    currency: catalog.currency ?? "USD",
    models: catalog.models.map((m) => ({
      id: m.id,
      provider: m.provider,
      model: m.model,
      aliases: m.aliases ? [...m.aliases].sort() : [],
      tiers: m.tiers.map((t) => ({
        upToContextTokens: t.upToContextTokens ?? null,
        inputPerMillion: t.inputPerMillion,
        outputPerMillion: t.outputPerMillion,
        cacheReadPerMillion: t.cacheReadPerMillion ?? null,
        cacheWritePerMillion: t.cacheWritePerMillion ?? null,
      })),
    })).sort((a, b) => a.id.localeCompare(b.id)),
  };
  return createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex");
}

export function parsePriceCatalog(text: string): PriceCatalog {
  const parsed = parse(text) as Record<string, unknown>;
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error("price catalog must be an object");
  }
  if (parsed.schema !== PRICES_SCHEMA) {
    throw new Error(`price catalog schema must be ${PRICES_SCHEMA}`);
  }
  if (typeof parsed.date !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(parsed.date)) {
    throw new Error("price catalog date must be YYYY-MM-DD");
  }
  if (!Number.isFinite(Date.parse(parsed.date)) || new Date(parsed.date).toISOString().slice(0, 10) !== parsed.date) {
    throw new Error("price catalog date must be a valid calendar date");
  }
  if (parsed.currency !== undefined && parsed.currency !== "USD") throw new Error("price catalog currency must be USD");
  if (typeof parsed.sha256 !== "string" || !/^(?:sha256:)?[a-f0-9]{64}$/.test(parsed.sha256)) {
    throw new Error("price catalog sha256 must be a 64-character hex digest");
  }
  if (!Array.isArray(parsed.models) || parsed.models.length === 0) {
    throw new Error("price catalog models must be a non-empty array");
  }
  const models: ModelPriceRow[] = parsed.models.map((item, index) => {
    if (!item || typeof item !== "object" || Array.isArray(item)) {
      throw new Error(`price catalog model at index ${index} must be an object`);
    }
    const m = item as Record<string, unknown>;
    if (typeof m.id !== "string" || !m.id) {
      throw new Error(`price catalog model at index ${index} must have a non-empty id`);
    }
    if (typeof m.provider !== "string" || !m.provider) {
      throw new Error(`price catalog model at index ${index} must have a non-empty provider`);
    }
    if (typeof m.model !== "string" || !m.model) {
      throw new Error(`price catalog model at index ${index} must have a non-empty model`);
    }
    const aliases = Array.isArray(m.aliases)
      ? m.aliases.filter((a): a is string => typeof a === "string" && Boolean(a))
      : undefined;
    if (!Array.isArray(m.tiers) || m.tiers.length === 0) {
      throw new Error(`price catalog model ${m.id} must have at least one tier`);
    }
    const rawTiers = m.tiers;
    let previousBound = 0;
    const tiers: PriceTier[] = rawTiers.map((tItem, tIndex) => {
      if (!tItem || typeof tItem !== "object" || Array.isArray(tItem)) {
        throw new Error(`tier at index ${tIndex} for model ${m.id} must be an object`);
      }
      const t = tItem as Record<string, unknown>;
      const rate = (field: string, short: string, required = false): number | null => {
        const value = t[field] !== undefined ? t[field] : t[short];
        if (value == null && !required) return null;
        if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
          throw new Error(`tier ${tIndex} for model ${m.id} requires finite non-negative ${field}`);
        }
        return value;
      };
      const bound = t.upToContextTokens ?? null;
      if (bound === null) {
        if (tIndex !== rawTiers.length - 1) throw new Error("unbounded price tier must be last");
      } else {
        if (typeof bound !== "number" || !Number.isSafeInteger(bound) || bound <= previousBound) throw new Error("price tier bounds must be positive and strictly increasing");
        previousBound = bound;
      }
      return {
        upToContextTokens: bound,
        inputPerMillion: rate("inputPerMillion", "input", true)!,
        outputPerMillion: rate("outputPerMillion", "output", true)!,
        cacheReadPerMillion: rate("cacheReadPerMillion", "cacheRead"),
        cacheWritePerMillion: rate("cacheWritePerMillion", "cacheWrite"),
      };
    });
    return {
      id: m.id,
      provider: m.provider,
      model: m.model,
      aliases,
      tiers,
    };
  });

  const catalog: PriceCatalog = {
    schema: PRICES_SCHEMA,
    date: parsed.date,
    sha256: parsed.sha256.replace(/^sha256:/, ""),
    currency: typeof parsed.currency === "string" ? parsed.currency : "USD",
    models,
  };
  if (new Set(models.map((row) => row.id)).size !== models.length) throw new Error("duplicate price catalog model id");
  if (hashPriceCatalog(catalog) !== catalog.sha256) throw new Error("price catalog hash mismatch");
  return catalog;
}

export function loadPriceCatalog(rootOrPath: string): PriceCatalog | undefined {
  const candidatePath = existsSync(join(rootOrPath, ".kxm", "prices.yaml"))
    ? join(rootOrPath, ".kxm", "prices.yaml")
    : (existsSync(join(rootOrPath, "prices.yaml"))
      ? join(rootOrPath, "prices.yaml")
      : (existsSync(rootOrPath) && statSync(rootOrPath).isFile() ? rootOrPath : undefined));

  if (!candidatePath || !existsSync(candidatePath)) {
    return undefined;
  }
  const content = readFileSync(candidatePath, "utf8");
  return parsePriceCatalog(content);
}

/** Result of a hash-verified, freshness-gated catalog load used for list estimates. */
export interface PriceCatalogEstimateSource {
  readonly catalog: PriceCatalog | undefined;
  readonly unavailable: boolean;
  readonly stale: boolean;
}

/**
 * Load a price catalog the way one-shot settlement does: re-parse injected
 * catalogs so the digest is checked, treat missing files as unknown, and drop
 * any snapshot whose date is not today. Callers must not throw after spend.
 */
export function loadPriceCatalogForEstimate(options?: {
  priceCatalog?: PriceCatalog | undefined;
  projectRoot?: string | undefined;
}): PriceCatalogEstimateSource {
  let catalog: PriceCatalog | undefined;
  try {
    catalog = options?.priceCatalog
      ? parsePriceCatalog(JSON.stringify(options.priceCatalog))
      : loadPriceCatalog(options?.projectRoot ?? process.cwd());
  } catch {
    return { catalog: undefined, unavailable: true, stale: false };
  }
  // Without a freshness-aware vendor feed, don't silently quote an older
  // local snapshot. Even today's hash-verified snapshot is only an estimate.
  if (catalog && catalog.date !== new Date().toISOString().slice(0, 10)) {
    return { catalog: undefined, unavailable: false, stale: true };
  }
  return { catalog, unavailable: false, stale: false };
}

/**
 * Stamp the existing project list-price file as today's estimate.
 * This does not fetch vendor rates. Until a catalog's date is today,
 * `loadPriceCatalogForEstimate` keeps the estimate unknown.
 */
export function acknowledgePriceCatalog(projectRoot: string, now = new Date()): PriceCatalog {
  const path = join(projectRoot, ".kxm", "prices.yaml");
  const loaded = loadPriceCatalog(projectRoot);
  if (!loaded) throw new Error("price catalog missing");
  const date = now.toISOString().slice(0, 10);
  const body = {
    schema: loaded.schema,
    date,
    currency: loaded.currency ?? "USD",
    models: loaded.models,
  };
  const stamped: PriceCatalog = { ...body, sha256: hashPriceCatalog(body) };
  writeFileSync(path, stringify({
    schema: stamped.schema,
    date: stamped.date,
    sha256: stamped.sha256,
    currency: stamped.currency ?? "USD",
    models: stamped.models,
  }), "utf8");
  const again = loadPriceCatalog(projectRoot);
  if (!again || again.date !== date || again.sha256 !== stamped.sha256) {
    throw new Error("price catalog acknowledge failed verification");
  }
  return again;
}

