import { readFileSync } from "node:fs";

import { rootPath } from "./config";

export const selfMediaConfigPath = rootPath("config", "selfmedia.yaml");
export const selfMediaConfigRef = "config/selfmedia.yaml";

export interface SelfMediaDeliveryTarget {
  readonly id: string;
  readonly node: string;
  readonly lane: string;
  readonly route: string;
  readonly ci: {
    readonly namespace: string;
    readonly pipeline: string;
    readonly pipelineRunPrefix: string;
    readonly serviceAccountName: string;
    readonly serviceAccountAutomount: false;
    readonly roleBindingName: string;
    readonly workspaceSize: string;
    readonly pipelineTimeout: string;
    readonly toolImage: string;
    readonly buildkitImage: string;
  };
  readonly source: {
    readonly repository: string;
    readonly worktreeRemote: string;
    readonly branch: string;
    readonly readUrl: string;
    readonly snapshotPrefix: string;
  };
  readonly build: Record<string, unknown> & {
    readonly dockerfile: string;
    readonly imageRepository: string;
    readonly networkMode: string;
    readonly proxy: Record<string, unknown>;
  };
  readonly gitops: Record<string, unknown> & {
    readonly readUrl: string;
    readonly writeUrl: string;
    readonly branch: string;
    readonly manifestPath: string;
    readonly releaseStatePath: string;
    readonly credentialSecretName: string;
    readonly credentialTokenKey: string;
    readonly credentialUsername: string;
  };
  readonly deployment: Record<string, unknown>;
}

export interface SelfMediaCutoverTarget {
  readonly id: string;
  readonly mode: "host-process-to-kubernetes";
  readonly source: {
    readonly workspace: string;
    readonly publicAddress: string;
    readonly healthUrl: string;
    readonly stopCommand: readonly string[];
    readonly startCommand: readonly string[];
    readonly dataPaths: readonly string[];
    readonly excludes: readonly string[];
  };
  readonly destination: {
    readonly route: string;
    readonly namespace: string;
    readonly application: string;
    readonly publicAddress: string;
    readonly healthUrl: string;
    readonly dataClaim: string;
    readonly codexClaim: string;
  };
  readonly prepare: {
    readonly requiresFinalConfirmation: true;
    readonly phases: readonly string[];
  };
  readonly rollback: {
    readonly phases: readonly string[];
    readonly dataPolicy: string;
  };
}

interface SelfMediaConfig {
  readonly defaultTargetId: string;
  readonly deliveryTargets: Readonly<Record<string, SelfMediaDeliveryTarget>>;
  readonly cutoverTargets: Readonly<Record<string, SelfMediaCutoverTarget>>;
}

export function readSelfMediaConfig(): SelfMediaConfig {
  const root = record(Bun.YAML.parse(readFileSync(selfMediaConfigPath, "utf8")), selfMediaConfigRef);
  if (integer(root.version, "version") !== 1) throw new Error(`${selfMediaConfigRef}.version must be 1`);
  if (text(root.kind, "kind") !== "selfmedia-platform-delivery") throw new Error(`${selfMediaConfigRef}.kind must be selfmedia-platform-delivery`);
  const defaults = record(root.defaults, "defaults");
  const delivery = record(root.delivery, "delivery");
  const cutover = record(root.cutover, "cutover");
  const deliveryRecords = record(delivery.targets, "delivery.targets");
  const cutoverRecords = record(cutover.targets, "cutover.targets");
  const deliveryTargets = Object.fromEntries(Object.entries(deliveryRecords).map(([id, value]) => [id, parseDeliveryTarget(id, record(value, `delivery.targets.${id}`))]));
  const cutoverTargets = Object.fromEntries(Object.entries(cutoverRecords).map(([id, value]) => [id, parseCutoverTarget(id, record(value, `cutover.targets.${id}`))]));
  const defaultTargetId = text(defaults.targetId, "defaults.targetId");
  if (deliveryTargets[defaultTargetId] === undefined || cutoverTargets[defaultTargetId] === undefined) {
    throw new Error(`${selfMediaConfigRef}.defaults.targetId must resolve in delivery.targets and cutover.targets`);
  }
  return { defaultTargetId, deliveryTargets, cutoverTargets };
}

export function resolveSelfMediaDeliveryTarget(targetId?: string | null): SelfMediaDeliveryTarget {
  const config = readSelfMediaConfig();
  const id = targetId ?? config.defaultTargetId;
  const target = config.deliveryTargets[id];
  if (target === undefined) throw new Error(`unknown selfmedia delivery target ${id}; known targets: ${Object.keys(config.deliveryTargets).join(", ")}`);
  return target;
}

export function resolveSelfMediaCutoverTarget(targetId?: string | null): SelfMediaCutoverTarget {
  const config = readSelfMediaConfig();
  const id = targetId ?? config.defaultTargetId;
  const target = config.cutoverTargets[id];
  if (target === undefined) throw new Error(`unknown selfmedia cutover target ${id}; known targets: ${Object.keys(config.cutoverTargets).join(", ")}`);
  return target;
}

export function selfMediaDeliveryConfigRef(targetId: string): string {
  return `${selfMediaConfigRef}#delivery.targets.${targetId}`;
}

export function selfMediaEffectiveDeployment(target: SelfMediaDeliveryTarget): Record<string, unknown> {
  return {
    ...structuredClone(target.deployment),
    delivery: {
      enabled: true,
      source: {
        branch: target.source.branch,
        snapshotPrefix: target.source.snapshotPrefix,
        readUrl: target.source.readUrl,
      },
      build: structuredClone(target.build),
      gitops: structuredClone(target.gitops),
    },
  };
}

function parseDeliveryTarget(id: string, value: Record<string, unknown>): SelfMediaDeliveryTarget {
  const path = `delivery.targets.${id}`;
  const ci = record(value.ci, `${path}.ci`);
  const source = record(value.source, `${path}.source`);
  const build = record(value.build, `${path}.build`);
  const proxy = record(build.proxy, `${path}.build.proxy`);
  const gitops = record(value.gitops, `${path}.gitops`);
  const deployment = record(value.deployment, `${path}.deployment`);
  const target = record(deployment.target, `${path}.deployment.target`);
  const publicExposure = record(deployment.publicExposure, `${path}.deployment.publicExposure`);
  const noProxy = stringArray(proxy.noProxy, `${path}.build.proxy.noProxy`);
  if (!noProxy.includes("hyueapi.com") || !noProxy.includes(".hyueapi.com")) throw new Error(`${path}.build.proxy.noProxy must preserve hyueapi.com and .hyueapi.com`);
  if (text(value.node, `${path}.node`) !== id) throw new Error(`${path}.node must match target id ${id}`);
  if (text(target.id, `${path}.deployment.target.id`) !== id) throw new Error(`${path}.deployment.target.id must match ${id}`);
  if (text(source.repository, `${path}.source.repository`) !== "pikainc/selfmedia") throw new Error(`${path}.source.repository must be pikainc/selfmedia`);
  if (!text(source.snapshotPrefix, `${path}.source.snapshotPrefix`).startsWith("refs/unidesk/snapshots/")) throw new Error(`${path}.source.snapshotPrefix must be immutable`);
  if (boolean(ci.serviceAccountAutomount, `${path}.ci.serviceAccountAutomount`) !== false) throw new Error(`${path}.ci.serviceAccountAutomount must be false`);
  if (boolean(publicExposure.enabled, `${path}.deployment.publicExposure.enabled`) !== true) throw new Error(`${path}.deployment.publicExposure.enabled must be true`);
  if (integer(publicExposure.hostPort, `${path}.deployment.publicExposure.hostPort`) !== 4317) throw new Error(`${path}.deployment.publicExposure.hostPort must be 4317`);
  validateAccessSecret(deployment, `${path}.deployment`);
  const gitopsWriteUrl = text(gitops.writeUrl, `${path}.gitops.writeUrl`);
  if (!gitopsWriteUrl.startsWith("http://gitea-http.")) throw new Error(`${path}.gitops.writeUrl must use internal Gitea authority`);
  return {
    id,
    node: id,
    lane: text(value.lane, `${path}.lane`),
    route: text(value.route, `${path}.route`),
    ci: {
      namespace: kubernetesName(ci.namespace, `${path}.ci.namespace`),
      pipeline: kubernetesName(ci.pipeline, `${path}.ci.pipeline`),
      pipelineRunPrefix: kubernetesName(ci.pipelineRunPrefix, `${path}.ci.pipelineRunPrefix`),
      serviceAccountName: kubernetesName(ci.serviceAccountName, `${path}.ci.serviceAccountName`),
      serviceAccountAutomount: false,
      roleBindingName: kubernetesName(ci.roleBindingName, `${path}.ci.roleBindingName`),
      workspaceSize: text(ci.workspaceSize, `${path}.ci.workspaceSize`),
      pipelineTimeout: text(ci.pipelineTimeout, `${path}.ci.pipelineTimeout`),
      toolImage: text(ci.toolImage, `${path}.ci.toolImage`),
      buildkitImage: text(ci.buildkitImage, `${path}.ci.buildkitImage`),
    },
    source: {
      repository: "pikainc/selfmedia",
      worktreeRemote: url(source.worktreeRemote, `${path}.source.worktreeRemote`),
      branch: text(source.branch, `${path}.source.branch`),
      readUrl: url(source.readUrl, `${path}.source.readUrl`),
      snapshotPrefix: text(source.snapshotPrefix, `${path}.source.snapshotPrefix`),
    },
    build: {
      ...structuredClone(build),
      dockerfile: relativePath(build.dockerfile, `${path}.build.dockerfile`),
      imageRepository: text(build.imageRepository, `${path}.build.imageRepository`),
      networkMode: text(build.networkMode, `${path}.build.networkMode`),
      proxy: structuredClone(proxy),
    },
    gitops: {
      ...structuredClone(gitops),
      readUrl: url(gitops.readUrl, `${path}.gitops.readUrl`),
      writeUrl: url(gitops.writeUrl, `${path}.gitops.writeUrl`),
      branch: text(gitops.branch, `${path}.gitops.branch`),
      manifestPath: relativePath(gitops.manifestPath, `${path}.gitops.manifestPath`),
      releaseStatePath: relativePath(gitops.releaseStatePath, `${path}.gitops.releaseStatePath`),
      credentialSecretName: kubernetesName(gitops.credentialSecretName, `${path}.gitops.credentialSecretName`),
      credentialTokenKey: text(gitops.credentialTokenKey, `${path}.gitops.credentialTokenKey`),
      credentialUsername: text(gitops.credentialUsername, `${path}.gitops.credentialUsername`),
    },
    deployment: structuredClone(deployment),
  };
}

function validateAccessSecret(deployment: Record<string, unknown>, path: string): void {
  const secrets = record(deployment.secrets, `${path}.secrets`);
  const access = record(secrets.access, `${path}.secrets.access`);
  const target = record(access.target, `${path}.secrets.access.target`);
  absolutePath(access.sourceRef, `${path}.secrets.access.sourceRef`);
  kubernetesName(target.secretName, `${path}.secrets.access.target.secretName`);
  if (boolean(target.readOnly, `${path}.secrets.access.target.readOnly`) !== true) {
    throw new Error(`${selfMediaConfigRef}.${path}.secrets.access.target.readOnly must be true`);
  }
  if (!Array.isArray(target.files) || target.files.length === 0) throw new Error(`${selfMediaConfigRef}.${path}.secrets.access.target.files must be a non-empty array`);
  const targetKeys = new Set<string>();
  const mountPaths = new Set<string>();
  target.files.forEach((value, index) => {
    const file = record(value, `${path}.secrets.access.target.files[${index}]`);
    const targetKey = text(file.targetKey, `${path}.secrets.access.target.files[${index}].targetKey`);
    const mountPath = absolutePath(file.mountPath, `${path}.secrets.access.target.files[${index}].mountPath`);
    if (targetKeys.has(targetKey)) throw new Error(`${selfMediaConfigRef}.${path}.secrets.access.target.files contains duplicate targetKey ${targetKey}`);
    if (mountPaths.has(mountPath)) throw new Error(`${selfMediaConfigRef}.${path}.secrets.access.target.files contains duplicate mountPath ${mountPath}`);
    targetKeys.add(targetKey);
    mountPaths.add(mountPath);
  });
}

function parseCutoverTarget(id: string, value: Record<string, unknown>): SelfMediaCutoverTarget {
  const path = `cutover.targets.${id}`;
  const source = record(value.source, `${path}.source`);
  const destination = record(value.destination, `${path}.destination`);
  const prepare = record(value.prepare, `${path}.prepare`);
  const rollback = record(value.rollback, `${path}.rollback`);
  const mode = text(value.mode, `${path}.mode`);
  if (mode !== "host-process-to-kubernetes") throw new Error(`${path}.mode must be host-process-to-kubernetes`);
  if (boolean(prepare.requiresFinalConfirmation, `${path}.prepare.requiresFinalConfirmation`) !== true) throw new Error(`${path}.prepare.requiresFinalConfirmation must be true`);
  return {
    id,
    mode,
    source: {
      workspace: absolutePath(source.workspace, `${path}.source.workspace`),
      publicAddress: url(source.publicAddress, `${path}.source.publicAddress`),
      healthUrl: url(source.healthUrl, `${path}.source.healthUrl`),
      stopCommand: stringArray(source.stopCommand, `${path}.source.stopCommand`),
      startCommand: stringArray(source.startCommand, `${path}.source.startCommand`),
      dataPaths: stringArray(source.dataPaths, `${path}.source.dataPaths`).map((item, index) => relativePath(item, `${path}.source.dataPaths[${index}]`)),
      excludes: stringArray(source.excludes, `${path}.source.excludes`).map((item, index) => relativePath(item, `${path}.source.excludes[${index}]`)),
    },
    destination: {
      route: text(destination.route, `${path}.destination.route`),
      namespace: kubernetesName(destination.namespace, `${path}.destination.namespace`),
      application: kubernetesName(destination.application, `${path}.destination.application`),
      publicAddress: url(destination.publicAddress, `${path}.destination.publicAddress`),
      healthUrl: url(destination.healthUrl, `${path}.destination.healthUrl`),
      dataClaim: kubernetesName(destination.dataClaim, `${path}.destination.dataClaim`),
      codexClaim: kubernetesName(destination.codexClaim, `${path}.destination.codexClaim`),
    },
    prepare: {
      requiresFinalConfirmation: true,
      phases: stringArray(prepare.phases, `${path}.prepare.phases`),
    },
    rollback: {
      phases: stringArray(rollback.phases, `${path}.rollback.phases`),
      dataPolicy: text(rollback.dataPolicy, `${path}.rollback.dataPolicy`),
    },
  };
}

function record(value: unknown, path: string): Record<string, unknown> {
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${selfMediaConfigRef}.${path} must be an object`);
  return value as Record<string, unknown>;
}

function text(value: unknown, path: string): string {
  if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${selfMediaConfigRef}.${path} must be a non-empty single-line string`);
  return value;
}

function integer(value: unknown, path: string): number {
  if (!Number.isInteger(value)) throw new Error(`${selfMediaConfigRef}.${path} must be an integer`);
  return value as number;
}

function boolean(value: unknown, path: string): boolean {
  if (typeof value !== "boolean") throw new Error(`${selfMediaConfigRef}.${path} must be a boolean`);
  return value;
}

function stringArray(value: unknown, path: string): string[] {
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0 || item.includes("\n"))) throw new Error(`${selfMediaConfigRef}.${path} must be an array of non-empty single-line strings`);
  return [...value] as string[];
}

function kubernetesName(value: unknown, path: string): string {
  const result = text(value, path);
  if (!/^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/u.test(result) || result.length > 63) throw new Error(`${selfMediaConfigRef}.${path} must be a Kubernetes name`);
  return result;
}

function absolutePath(value: unknown, path: string): string {
  const result = text(value, path);
  if (!result.startsWith("/") || result.split("/").includes("..")) throw new Error(`${selfMediaConfigRef}.${path} must be an absolute path without ..`);
  return result;
}

function relativePath(value: unknown, path: string): string {
  const result = text(value, path);
  if (result.startsWith("/") || result.split("/").includes("..")) throw new Error(`${selfMediaConfigRef}.${path} must be a relative path without ..`);
  return result;
}

function url(value: unknown, path: string): string {
  const result = text(value, path);
  const parsed = new URL(result);
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${selfMediaConfigRef}.${path} must use http or https`);
  if (parsed.username.length > 0 || parsed.password.length > 0) throw new Error(`${selfMediaConfigRef}.${path} must not contain credentials`);
  return result;
}
