import { createHash, randomUUID } from "node:crypto";
import {
  accessSync,
  constants,
  existsSync,
  lstatSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  realpathSync,
  renameSync,
  rmSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";

const require = createRequire(import.meta.url);
const Database = require("better-sqlite3") as typeof import("better-sqlite3");

const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i;
const fixedDirectories = [
  "db",
  "content/references",
  "content/generated",
  "content/exports",
  "managed-assets",
  "derived-assets",
  "staging",
  "logs/api",
  "logs/worker",
  "logs/supervisor",
] as const;

export function ensureLocalDataRuntimeDirectories(dataRoot: string) {
  for (const directory of fixedDirectories) {
    mkdirSync(join(resolve(dataRoot), directory), { recursive: true });
  }
}

export const DATA_TRANSFER_POLICY = {
  allowed_downloads: ["original_generation", "jpg", "png"],
  application_backup: false,
  business_import: false,
  editable_project_archive: false,
  p0b_migration: false,
  recovery: false,
} as const;

export interface LocalDataRootBoundaries {
  downloadsRoot: string;
  programRoot: string;
  projectRoots: string[];
  readOnlyAssetRoots: string[];
  repositoryRoot: string;
}

export type LocalDataRootRejectionReason =
  | "repository_root"
  | "program_root"
  | "project_root"
  | "downloads_root"
  | "read_only_asset_root"
  | "symbolic_link";

export type LocalDataRootValidation =
  | { ok: true; normalized_path: string }
  | { ok: false; reason: LocalDataRootRejectionReason };

interface InstanceConfiguration {
  data_root: string;
  initialized: true;
  instance_id: string;
  schema_version: 1;
}

export function readConfiguredLocalDataRoot(configFile = defaultInstanceConfigPath()) {
  const configuration = JSON.parse(readFileSync(configFile, "utf8")) as Record<string, unknown>;
  const candidate = configuration.data_root ?? configuration.local_data_root;
  if (typeof candidate !== "string" || !isAbsolute(candidate)) throw new Error("data_root_configuration_invalid");
  return resolve(candidate);
}

export function readConfiguredAssetRoot(configFile = defaultInstanceConfigPath()) {
  const configuration = JSON.parse(readFileSync(configFile, "utf8")) as Record<string, unknown>;
  if (typeof configuration.asset_root !== "string" || !isAbsolute(configuration.asset_root)) {
    throw new Error("asset_root_configuration_invalid");
  }
  return resolve(configuration.asset_root);
}

export interface ValidatedReadOnlyAssetRoot {
  absolute_root: string;
  ok: true;
  root_ref: string;
}

export interface PublicAssetEntry {
  assetId: string;
  mimeType: string;
  relativePath: string;
  resourceVersion: string;
  rootRef: string;
  sha256: string;
}

export interface PublicAssetPayload {
  assetId: string;
  bytes: Buffer;
  mimeType: string;
  resourceVersion: string;
  sha256: string;
}

export interface PublicAssetResolver {
  read(resourceVersion: string, assetId: string): PublicAssetPayload | undefined;
}

export function defaultLocalDataRoot(environment: NodeJS.ProcessEnv = process.env) {
  const localAppData = environment.LOCALAPPDATA;
  if (!localAppData || !isAbsolute(localAppData)) throw new Error("local_app_data_unavailable");
  return join(localAppData, "Dada", "P0A", "data");
}

export function defaultInstanceConfigPath(environment: NodeJS.ProcessEnv = process.env) {
  const localAppData = environment.LOCALAPPDATA;
  if (!localAppData || !isAbsolute(localAppData)) throw new Error("local_app_data_unavailable");
  return join(localAppData, "Dada", "P0A", "config", "instance.json");
}

function comparisonPath(path: string) {
  const normalized = resolve(path).replace(/[\\/]+$/, "");
  return process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
}

function isSameOrWithin(candidate: string, root: string) {
  const candidatePath = comparisonPath(candidate);
  const rootPath = comparisonPath(root);
  const child = relative(rootPath, candidatePath);
  return child === "" || (!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child));
}

function pathsOverlap(left: string, right: string) {
  return isSameOrWithin(left, right) || isSameOrWithin(right, left);
}

function containsSymbolicLink(path: string) {
  const absolute = resolve(path);
  const parsed = parse(absolute);
  let current = parsed.root;
  const segments = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
  for (const segment of segments) {
    current = join(current, segment);
    if (!existsSync(current)) break;
    if (lstatSync(current).isSymbolicLink()) return true;
  }
  return false;
}

function canonicalExistingPath(path: string) {
  const absolute = resolve(path);
  let existing = absolute;
  const missing: string[] = [];
  while (!existsSync(existing)) {
    const parent = dirname(existing);
    if (parent === existing) break;
    missing.unshift(existing.slice(parent.length + 1));
    existing = parent;
  }
  const canonical = existsSync(existing) ? realpathSync.native(existing) : existing;
  return resolve(canonical, ...missing);
}

export function validateLocalDataRoot(
  candidate: string,
  boundaries: LocalDataRootBoundaries,
): LocalDataRootValidation {
  const absolute = resolve(candidate);
  if (containsSymbolicLink(absolute)) return { ok: false, reason: "symbolic_link" };
  const canonical = canonicalExistingPath(absolute);
  const restricted: ReadonlyArray<readonly [LocalDataRootRejectionReason, string]> = [
    ["repository_root", boundaries.repositoryRoot],
    ["program_root", boundaries.programRoot],
    ...boundaries.projectRoots.map((root) => ["project_root", root] as const),
    ["downloads_root", boundaries.downloadsRoot],
    ...boundaries.readOnlyAssetRoots.map((root) => ["read_only_asset_root", root] as const),
  ];
  for (const [reason, root] of restricted) {
    if (pathsOverlap(canonical, canonicalExistingPath(root))) return { ok: false, reason };
  }
  return { normalized_path: absolute, ok: true };
}

export function resolvePathWithinRoot(root: string, objectKey: string) {
  if (!objectKey || isAbsolute(objectKey) || objectKey.split(/[\\/]/).includes("..")) {
    throw new Error("path_escape");
  }
  if (containsSymbolicLink(root)) throw new Error("symbolic_link");
  const target = resolve(root, objectKey);
  if (!isSameOrWithin(target, root) || containsSymbolicLink(target)) throw new Error("path_escape");
  const canonicalRoot = canonicalExistingPath(root);
  const canonicalTarget = canonicalExistingPath(target);
  if (!isSameOrWithin(canonicalTarget, canonicalRoot)) throw new Error("path_escape");
  return target;
}

function openInstanceDatabase(databasePath: string) {
  const nativeBinding = process.env.DADA_SQLITE_NATIVE_BINDING;
  const database = new Database(databasePath, nativeBinding ? { nativeBinding } : undefined);
  database.pragma("journal_mode = WAL");
  database.exec(`
    CREATE TABLE instance_metadata (
      singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
      schema_version INTEGER NOT NULL CHECK (schema_version = 1)
    );
    INSERT INTO instance_metadata (singleton, schema_version) VALUES (1, 1);
  `);
  database.close();
}

export function initializeLocalDataRoot(input: {
  boundaries: LocalDataRootBoundaries;
  configFile: string;
  dataRoot: string;
}) {
  const validation = validateLocalDataRoot(input.dataRoot, input.boundaries);
  if (!validation.ok) throw new Error(validation.reason);
  if (existsSync(input.configFile)) throw new Error("already_initialized");
  if (existsSync(validation.normalized_path) && readdirSync(validation.normalized_path).length > 0) {
    throw new Error("data_root_not_empty");
  }

  const createdRoot = !existsSync(validation.normalized_path);
  try {
    ensureLocalDataRuntimeDirectories(validation.normalized_path);
    openInstanceDatabase(join(validation.normalized_path, "db", "dada.sqlite3"));
    const configuration: InstanceConfiguration = {
      data_root: validation.normalized_path,
      initialized: true,
      instance_id: randomUUID(),
      schema_version: 1,
    };
    mkdirSync(dirname(input.configFile), { recursive: true });
    const temporaryConfig = `${input.configFile}.${randomUUID()}.tmp`;
    writeFileSync(temporaryConfig, `${JSON.stringify(configuration, null, 2)}\n`, { flag: "wx" });
    renameSync(temporaryConfig, input.configFile);
    return {
      database: "db/dada.sqlite3",
      directories: [...fixedDirectories],
      status: "ready" as const,
    };
  } catch (error) {
    if (createdRoot && existsSync(validation.normalized_path)) {
      rmSync(validation.normalized_path, { force: true, recursive: true });
    }
    throw error;
  }
}

export function inspectInitializedLocalDataRoot(input: {
  boundaries: LocalDataRootBoundaries;
  configFile: string;
}) {
  const configuration = JSON.parse(readFileSync(input.configFile, "utf8")) as InstanceConfiguration;
  const validation = validateLocalDataRoot(configuration.data_root, input.boundaries);
  if (!validation.ok) throw new Error(validation.reason);
  const rootExists = existsSync(validation.normalized_path) && statSync(validation.normalized_path).isDirectory();
  const databasePath = join(validation.normalized_path, "db", "dada.sqlite3");
  const databaseExists = rootExists && existsSync(databasePath) && statSync(databasePath).isFile();
  if (!rootExists || !databaseExists) {
    return {
      database_exists: databaseExists,
      root_exists: rootExists,
      status: "data_missing" as const,
    };
  }
  let writable = true;
  try {
    accessSync(validation.normalized_path, constants.R_OK | constants.W_OK);
  } catch {
    writable = false;
  }
  return {
    database_exists: true,
    root_exists: true,
    status: "ready" as const,
    writable,
  };
}

export function validateReadOnlyAssetRoot(input: {
  dataRoot: string;
  expectedSha256: string;
  manifestRelativePath: string;
  root: string;
  rootRef: string;
}): ValidatedReadOnlyAssetRoot | { ok: false; reason: string } {
  const absoluteRoot = resolve(input.root);
  if (!existsSync(absoluteRoot) || !statSync(absoluteRoot).isDirectory()) {
    return { ok: false, reason: "asset_root_missing" };
  }
  if (containsSymbolicLink(absoluteRoot)) return { ok: false, reason: "symbolic_link" };
  if (pathsOverlap(absoluteRoot, input.dataRoot)) return { ok: false, reason: "data_root_overlap" };
  let manifestPath: string;
  try {
    manifestPath = resolvePathWithinRoot(absoluteRoot, input.manifestRelativePath);
  } catch {
    return { ok: false, reason: "manifest_path_invalid" };
  }
  if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
    return { ok: false, reason: "manifest_missing" };
  }
  const actualSha256 = createHash("sha256").update(readFileSync(manifestPath)).digest("hex");
  if (actualSha256.toLowerCase() !== input.expectedSha256.toLowerCase()) {
    return { ok: false, reason: "manifest_hash_invalid" };
  }
  return { absolute_root: absoluteRoot, ok: true, root_ref: input.rootRef };
}

export function createPublicAssetResolver(input: {
  entries: PublicAssetEntry[];
  roots: ValidatedReadOnlyAssetRoot[];
}): PublicAssetResolver {
  const roots = new Map(input.roots.map((root) => [root.root_ref, root.absolute_root]));
  const entries = new Map<string, PublicAssetEntry>();
  for (const entry of input.entries) {
    const key = `${entry.resourceVersion}\u0000${entry.assetId}`;
    if (!assetIdPattern.test(entry.assetId) || entries.has(key)) throw new Error("asset_id_invalid");
    if (!roots.has(entry.rootRef)) throw new Error("asset_root_unvalidated");
    if (!/^[a-z0-9][a-z0-9._-]{0,79}$/i.test(entry.resourceVersion)) throw new Error("resource_version_invalid");
    if (!/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i.test(entry.mimeType)) throw new Error("mime_type_invalid");
    entries.set(key, { ...entry });
  }

  return {
    read(resourceVersion, assetId) {
      if (!assetIdPattern.test(assetId)) return undefined;
      const entry = entries.get(`${resourceVersion}\u0000${assetId}`);
      if (!entry) return undefined;
      const root = roots.get(entry.rootRef);
      if (!root) return undefined;
      let path: string;
      try {
        path = resolvePathWithinRoot(root, entry.relativePath);
      } catch {
        return undefined;
      }
      if (!existsSync(path) || !statSync(path).isFile()) return undefined;
      const bytes = readFileSync(path);
      const sha256 = createHash("sha256").update(bytes).digest("hex");
      if (sha256.toLowerCase() !== entry.sha256.toLowerCase()) return undefined;
      return {
        assetId: entry.assetId,
        bytes,
        mimeType: entry.mimeType,
        resourceVersion: entry.resourceVersion,
        sha256,
      };
    },
  };
}
