import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";

import {
  createPublicAssetResolver,
  readConfiguredAssetRoot,
  validateReadOnlyAssetRoot,
  type PublicAssetEntry,
  type PublicAssetResolver,
} from "./local-data-root.js";

const rootRef = "p0a_runtime_assets";
const schemaVersion = "DadaRuntimeAssets/v1";
const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
const releasePattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i;
const shaPattern = /^[a-f0-9]{64}$/i;

export interface RuntimeAssetState {
  checked_at: string;
  configured: boolean;
  pause_reason: "asset_manifest_invalid" | "asset_root_missing" | "asset_root_state_missing" | null;
  status: "active" | "unavailable";
}

export interface LoadedRuntimeAssets {
  publicAssets?: PublicAssetResolver;
  state: RuntimeAssetState;
}

function parseRuntimeManifest(bytes: Buffer): PublicAssetEntry[] {
  const value = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
  if (value.schema_version !== schemaVersion || value.source !== "external_read_only" || value.root_ref !== rootRef) {
    throw new Error("runtime_asset_manifest_invalid");
  }
  if (!Array.isArray(value.entries) || value.entries.length === 0) throw new Error("runtime_asset_manifest_invalid");
  return value.entries.map((candidate) => {
    if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) throw new Error("runtime_asset_manifest_invalid");
    const entry = candidate as Record<string, unknown>;
    if (
      typeof entry.assetId !== "string" || !assetIdPattern.test(entry.assetId)
      || typeof entry.mimeType !== "string" || !/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)
      || typeof entry.relativePath !== "string" || entry.relativePath.includes("\\") || entry.relativePath.split("/").includes("..")
      || typeof entry.resourceVersion !== "string" || !releasePattern.test(entry.resourceVersion)
      || entry.rootRef !== rootRef
      || typeof entry.sha256 !== "string" || !shaPattern.test(entry.sha256)
    ) throw new Error("runtime_asset_manifest_invalid");
    return entry as unknown as PublicAssetEntry;
  });
}

function unavailable(
  configured: boolean,
  pauseReason: Exclude<RuntimeAssetState["pause_reason"], null>,
  checkedAt: string,
): LoadedRuntimeAssets {
  return { state: { checked_at: checkedAt, configured, pause_reason: pauseReason, status: "unavailable" } };
}

export function loadConfiguredRuntimeAssets(input: {
  configFile: string;
  dataRoot: string;
  trustedManifestPath: string;
  clock?: () => number;
}): LoadedRuntimeAssets {
  const checkedAt = new Date((input.clock ?? Date.now)()).toISOString();
  let assetRoot: string;
  try {
    assetRoot = readConfiguredAssetRoot(input.configFile);
  } catch {
    return unavailable(false, "asset_root_state_missing", checkedAt);
  }
  if (!existsSync(input.trustedManifestPath)) return unavailable(true, "asset_manifest_invalid", checkedAt);
  try {
    const trustedBytes = readFileSync(input.trustedManifestPath);
    const entries = parseRuntimeManifest(trustedBytes);
    const validatedRoot = validateReadOnlyAssetRoot({
      dataRoot: input.dataRoot,
      expectedSha256: createHash("sha256").update(trustedBytes).digest("hex"),
      manifestRelativePath: "manifest.json",
      root: assetRoot,
      rootRef,
    });
    if (!validatedRoot.ok) {
      return unavailable(true, validatedRoot.reason === "asset_root_missing" ? "asset_root_missing" : "asset_manifest_invalid", checkedAt);
    }
    return {
      publicAssets: createPublicAssetResolver({ entries, roots: [validatedRoot] }),
      state: { checked_at: checkedAt, configured: true, pause_reason: null, status: "active" },
    };
  } catch {
    return unavailable(true, "asset_manifest_invalid", checkedAt);
  }
}
