import { createHash } from "node:crypto";
import {
  existsSync,
  linkSync,
  lstatSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  realpathSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";

export const P0A_RUNTIME_ASSET_ROOT_REF = "p0a_runtime_assets";
export const RUNTIME_ASSET_MANIFEST_SCHEMA = "DadaRuntimeAssets/v1";

const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
const mimePattern = /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i;
const releasePattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i;
const shaPattern = /^[a-f0-9]{64}$/i;
const fontMimeTypes = new Map([
  [".otf", "font/otf"],
  [".ttf", "font/ttf"],
  [".woff", "font/woff"],
  [".woff2", "font/woff2"],
]);

function sha256(bytes) {
  return createHash("sha256").update(bytes).digest("hex");
}

function fileSha256(path) {
  return sha256(readFileSync(path));
}

function stableEntries(entries) {
  return entries.map((entry) => {
    if (!entry || typeof entry !== "object") throw new Error("runtime_asset_entry_invalid");
    if (!assetIdPattern.test(entry.assetId)) throw new Error("runtime_asset_id_invalid");
    if (!mimePattern.test(entry.mimeType)) throw new Error("runtime_asset_mime_invalid");
    if (!releasePattern.test(entry.resourceVersion)) throw new Error("runtime_asset_version_invalid");
    if (entry.rootRef !== P0A_RUNTIME_ASSET_ROOT_REF) throw new Error("runtime_asset_root_ref_invalid");
    if (!shaPattern.test(entry.sha256)) throw new Error("runtime_asset_sha256_invalid");
    if (
      typeof entry.relativePath !== "string"
      || isAbsolute(entry.relativePath)
      || entry.relativePath.includes("\\")
      || entry.relativePath.split("/").some((part) => part === "" || part === "..")
    ) throw new Error("runtime_asset_relative_path_invalid");
    return { ...entry, sha256: entry.sha256.toLowerCase() };
  }).sort((left, right) => {
    const byVersion = left.resourceVersion.localeCompare(right.resourceVersion);
    return byVersion || left.assetId.localeCompare(right.assetId);
  });
}

function derivedCounts(entries) {
  return {
    dynamic_fonts: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^[a-f0-9]{32}$/.test(entry.assetId)).length,
    dynamic_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^DYN\d{3}-/.test(entry.assetId)).length,
    font_panel_items: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^FONT\d{3}$/.test(entry.assetId)).length,
    static_stickers: entries.filter((entry) => entry.resourceVersion === "p0a-static-v1" && /^STK\d{3,4}$/.test(entry.assetId)).length,
  };
}

export function createRuntimeAssetManifest({ counts, entries, sourceManifestSha256 }) {
  const normalizedEntries = stableEntries(entries);
  const keys = new Set();
  const paths = new Set();
  for (const entry of normalizedEntries) {
    const key = `${entry.resourceVersion}\u0000${entry.assetId}`;
    if (keys.has(key)) throw new Error("runtime_asset_id_duplicate");
    if (paths.has(entry.relativePath)) throw new Error("runtime_asset_path_duplicate");
    keys.add(key);
    paths.add(entry.relativePath);
  }
  const actualCounts = derivedCounts(normalizedEntries);
  if (JSON.stringify(counts) !== JSON.stringify(actualCounts)) throw new Error("runtime_asset_counts_invalid");
  if (sourceManifestSha256 !== undefined && !shaPattern.test(sourceManifestSha256)) {
    throw new Error("runtime_asset_source_manifest_sha256_invalid");
  }
  return {
    counts: actualCounts,
    entries: normalizedEntries,
    root_ref: P0A_RUNTIME_ASSET_ROOT_REF,
    schema_version: RUNTIME_ASSET_MANIFEST_SCHEMA,
    source: "external_read_only",
    ...(sourceManifestSha256 ? { source_manifest_sha256: sourceManifestSha256.toLowerCase() } : {}),
  };
}

export function readRuntimeAssetManifest(path) {
  const value = JSON.parse(readFileSync(path, "utf8"));
  if (
    value?.schema_version !== RUNTIME_ASSET_MANIFEST_SCHEMA
    || value?.source !== "external_read_only"
    || value?.root_ref !== P0A_RUNTIME_ASSET_ROOT_REF
    || !Array.isArray(value.entries)
  ) throw new Error("runtime_asset_manifest_invalid");
  return createRuntimeAssetManifest({
    counts: value.counts,
    entries: value.entries,
    ...(value.source_manifest_sha256 ? { sourceManifestSha256: value.source_manifest_sha256 } : {}),
  });
}

export function serializeRuntimeAssetManifest(manifest) {
  return `${JSON.stringify(manifest, null, 2)}\n`;
}

export function writeRuntimeAssetManifest(path, manifest) {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, serializeRuntimeAssetManifest(manifest));
}

function targetWithinRoot(root, relativePath) {
  const absoluteRoot = resolve(root);
  const target = resolve(absoluteRoot, ...relativePath.split("/"));
  if (target === absoluteRoot || !target.startsWith(`${absoluteRoot}${sep}`)) throw new Error("asset_target_path_invalid");
  return target;
}

function sameFile(left, right) {
  const leftStat = statSync(left);
  const rightStat = statSync(right);
  return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
}

export function deployRuntimeAssetPlan({ assetRoot, manifest, resources }) {
  if (!isAbsolute(assetRoot)) throw new Error("asset_root_must_be_absolute");
  const normalizedManifest = createRuntimeAssetManifest({
    counts: manifest.counts,
    entries: manifest.entries,
    ...(manifest.source_manifest_sha256 ? { sourceManifestSha256: manifest.source_manifest_sha256 } : {}),
  });
  const entries = new Map(normalizedManifest.entries.map((entry) => [`${entry.resourceVersion}\u0000${entry.assetId}`, entry]));
  if (resources.length !== entries.size) throw new Error("asset_resource_plan_incomplete");
  mkdirSync(assetRoot, { recursive: true });
  for (const resource of resources) {
    const key = `${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`;
    const entry = entries.get(key);
    if (!entry || JSON.stringify(entry) !== JSON.stringify({ ...resource.entry, sha256: resource.entry.sha256.toLowerCase() })) {
      throw new Error("asset_resource_plan_mismatch");
    }
    if (!existsSync(resource.sourcePath) || !statSync(resource.sourcePath).isFile() || lstatSync(resource.sourcePath).isSymbolicLink()) {
      throw new Error("asset_source_invalid");
    }
    if (fileSha256(resource.sourcePath) !== entry.sha256) throw new Error("asset_source_hash_invalid");
    const targetPath = targetWithinRoot(assetRoot, entry.relativePath);
    mkdirSync(dirname(targetPath), { recursive: true });
    if (existsSync(targetPath)) {
      if (fileSha256(targetPath) !== entry.sha256) throw new Error("asset_target_conflict");
      if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_target_not_hardlink");
      continue;
    }
    try {
      linkSync(resource.sourcePath, targetPath);
    } catch (error) {
      if (error && typeof error === "object" && "code" in error && error.code === "EXDEV") {
        throw new Error("asset_hardlink_volume_mismatch");
      }
      throw error;
    }
    if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_hardlink_verification_failed");
  }
  writeRuntimeAssetManifest(join(assetRoot, "manifest.json"), normalizedManifest);
  return { linked_files: resources.length, manifest: normalizedManifest, status: "ready" };
}

function oneDirectoryWithPrefix(root, prefix) {
  const matches = readdirSync(root, { withFileTypes: true })
    .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${prefix}_`));
  if (matches.length !== 1) throw new Error(`runtime_asset_source_directory_invalid:${prefix}`);
  return join(root, matches[0].name);
}

function oneSupportedFont(root) {
  const matches = readdirSync(root, { withFileTypes: true })
    .filter((entry) => entry.isFile() && fontMimeTypes.has(extname(entry.name).toLowerCase()));
  if (matches.length !== 1) throw new Error(`runtime_font_source_invalid:${basename(root)}`);
  return join(root, matches[0].name);
}

function entryFor(sourcePath, assetId, resourceVersion, relativePath, mimeType) {
  return {
    assetId,
    mimeType,
    relativePath,
    resourceVersion,
    rootRef: P0A_RUNTIME_ASSET_ROOT_REF,
    sha256: fileSha256(sourcePath),
  };
}

function dynamicMetadata(templateRoot, descriptor, field) {
  const templateDirectory = join(templateRoot, descriptor.templateId);
  const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8"));
  if (!Array.isArray(metadata?.files?.[field]) || !metadata.files[field].includes(descriptor.sourceReference)) {
    throw new Error(`runtime_dynamic_reference_invalid:${descriptor.assetId}`);
  }
  return templateDirectory;
}

export async function buildP0aRuntimeAssetPlan({ replicationRoot }) {
  const [{ compileStaticStickerCatalog }, registry] = await Promise.all([
    import("../../packages/asset-compiler/dist/index.js"),
    import("../../packages/template-registry/dist/index.js"),
  ]);
  const compilerOutput = mkdtempSync(join(tmpdir(), "dada-runtime-asset-plan-"));
  try {
    const staticSourceRoot = join(replicationRoot, "sticker_normal");
    const staticResult = compileStaticStickerCatalog({
      outputDirectory: compilerOutput,
      releaseVersion: registry.P0A_STATIC_STICKER_RELEASE_VERSION,
      sourceRoot: staticSourceRoot,
    });
    const resources = staticResult.catalog.items.map((item) => {
      const sourcePath = join(staticSourceRoot, ...item.relative_path.split("/"));
      const entry = entryFor(
        sourcePath,
        item.stable_id,
        registry.P0A_STATIC_STICKER_RELEASE_VERSION,
        `${registry.P0A_STATIC_STICKER_RELEASE_VERSION}/${item.stable_id}.png`,
        "image/png",
      );
      if (entry.sha256 !== item.sha256.toLowerCase()) throw new Error(`static_sticker_hash_invalid:${item.stable_id}`);
      return { entry, sourcePath };
    });

    const fontPackagesRoot = join(
      replicationRoot,
      "sticker_text",
      "字体",
      "面板全量采集",
      "font_panel_full_20260722",
      "resources",
      "font_packages",
    );
    for (const assetId of registry.P0A_REQUIRED_FONT_PANEL_IDS) {
      const packageDirectory = oneDirectoryWithPrefix(fontPackagesRoot, assetId);
      const sourcePath = oneSupportedFont(join(packageDirectory, "font_files"));
      const extension = extname(sourcePath).toLowerCase();
      resources.push({
        entry: entryFor(
          sourcePath,
          assetId,
          registry.P0A_COMPLEX_RELEASE_VERSION,
          `${registry.P0A_COMPLEX_RELEASE_VERSION}/${assetId}${extension}`,
          fontMimeTypes.get(extension),
        ),
        sourcePath,
      });
    }

    const templateRoot = join(replicationRoot, "sticker_interactive", "单模板归档", "templates");
    for (const descriptor of registry.P0A_DYNAMIC_RUNTIME_FONT_SOURCES) {
      const templateDirectory = dynamicMetadata(templateRoot, descriptor, "fonts");
      const sourcePath = oneSupportedFont(join(templateDirectory, ...descriptor.sourceReference.split("/")));
      const extension = extname(sourcePath).toLowerCase();
      resources.push({
        entry: entryFor(
          sourcePath,
          descriptor.assetId,
          registry.P0A_COMPLEX_RELEASE_VERSION,
          `${registry.P0A_COMPLEX_RELEASE_VERSION}/${descriptor.assetId}${extension}`,
          fontMimeTypes.get(extension),
        ),
        sourcePath,
      });
    }
    for (const descriptor of registry.P0A_DYNAMIC_RUNTIME_IMAGE_SOURCES) {
      const templateDirectory = dynamicMetadata(templateRoot, descriptor, "images");
      const sourcePath = join(templateDirectory, ...descriptor.sourceReference.split("/"));
      if (!existsSync(sourcePath) || extname(sourcePath).toLowerCase() !== ".png") {
        throw new Error(`runtime_dynamic_image_invalid:${descriptor.assetId}`);
      }
      resources.push({
        entry: entryFor(
          sourcePath,
          descriptor.assetId,
          registry.P0A_COMPLEX_RELEASE_VERSION,
          `${registry.P0A_COMPLEX_RELEASE_VERSION}/${descriptor.assetId}.png`,
          "image/png",
        ),
        sourcePath,
      });
    }

    const manifestPath = join(replicationRoot, "sticker_web_handoff", "sticker_web_catalog_manifest.json");
    const manifest = createRuntimeAssetManifest({
      counts: {
        dynamic_fonts: registry.P0A_DYNAMIC_RUNTIME_FONT_SOURCES.length,
        dynamic_images: registry.P0A_DYNAMIC_RUNTIME_IMAGE_SOURCES.length,
        font_panel_items: registry.P0A_REQUIRED_FONT_PANEL_IDS.length,
        static_stickers: staticResult.catalog.count,
      },
      entries: resources.map((resource) => resource.entry),
      sourceManifestSha256: fileSha256(manifestPath),
    });
    const resourcesByKey = new Map(resources.map((resource) => [`${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`, resource]));
    return {
      manifest,
      resources: manifest.entries.map((entry) => resourcesByKey.get(`${entry.resourceVersion}\u0000${entry.assetId}`)),
    };
  } finally {
    rmSync(compilerOutput, { force: true, recursive: true });
  }
}

export function defaultReplicationRoot(environment = process.env) {
  if (!environment.USERPROFILE || !isAbsolute(environment.USERPROFILE)) throw new Error("user_profile_unavailable");
  return join(environment.USERPROFILE, "Desktop", "sticker_web_replication_assets");
}
