import { isAbsolute, normalize, relative } from "node:path";
import { readServerBinding, type ServerBinding, type ServerEnv } from "../settings/server.js";

export type DeploymentEnv = ServerEnv;

export interface DeploymentPreflightInput {
  readonly baseDir: string;
  readonly hubDir: string;
  readonly serviceHome: string;
  readonly stateDir: string;
  readonly cacheDir: string;
  readonly workspaceRoot: string;
  readonly expectedServiceUnit: string;
  readonly env: DeploymentEnv;
}

export interface DeploymentPreflightResult {
  readonly bind: ServerBinding;
  readonly databaseUrl: string;
  readonly cphBin: string;
}

export class DeploymentPreflightError extends Error {
  readonly problems: readonly string[];

  constructor(problems: readonly string[]) {
    super(`deployment preflight failed:\n${problems.map((problem) => `- ${problem}`).join("\n")}`);
    this.name = "DeploymentPreflightError";
    this.problems = problems;
  }
}

/**
 * Validate the production values shared by the installer and the Hub runtime.
 * Inputs are required to be absolute normalized paths so callers can first
 * resolve existing paths physically and make symlink traversal observable.
 */
export function validateDeploymentPreflight(input: DeploymentPreflightInput): DeploymentPreflightResult {
  const problems: string[] = [];
  const paths = [
    ["deployment root", input.baseDir],
    ["Hub directory", input.hubDir],
    ["service home", input.serviceHome],
    ["state directory", input.stateDir],
    ["cache directory", input.cacheDir],
    ["workspace root", input.workspaceRoot],
  ] as const;

  for (const [label, value] of paths) {
    if (!isAbsoluteNormalized(value)) {
      problems.push(`${label} must be an absolute normalized path`);
    }
  }

  if (isAbsoluteNormalized(input.baseDir) && isAbsoluteNormalized(input.hubDir)) {
    if (!isStrictDescendant(input.baseDir, input.hubDir)) {
      problems.push("Hub directory must be inside deployment root");
    }
  }

  if (isAbsoluteNormalized(input.baseDir)) {
    for (const [label, value] of paths.slice(2)) {
      if (isAbsoluteNormalized(value) && pathsOverlap(input.baseDir, value)) {
        problems.push(`${label} must not overlap deployment root`);
      }
    }
  }

  const envWorkspaceRoot = readRequired(input.env, "HUB_PROJECT_WORKSPACE_ROOT", problems);
  if (envWorkspaceRoot !== undefined) {
    if (!isAbsoluteNormalized(envWorkspaceRoot)) {
      problems.push("HUB_PROJECT_WORKSPACE_ROOT must be an absolute normalized path");
    } else if (isAbsoluteNormalized(input.workspaceRoot) && envWorkspaceRoot !== input.workspaceRoot) {
      problems.push("HUB_PROJECT_WORKSPACE_ROOT must equal provisioned workspace root");
    }
  }

  if (input.env["NODE_ENV"] !== "production") {
    problems.push("NODE_ENV must equal production");
  }

  const databaseUrl = readRequired(input.env, "DATABASE_URL", problems);
  if (databaseUrl !== undefined && !isPostgresUrl(databaseUrl)) {
    problems.push("DATABASE_URL must be a valid postgresql:// or postgres:// URL");
  }

  for (const name of ["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"] as const) {
    if (Object.hasOwn(input.env, name)) {
      problems.push(`${name} must not be set; configure Organization Provider Connections`);
    }
  }

  readRequired(input.env, "HUB_SILO_ORGANIZATION_ID", problems);
  const serviceUnit = readRequired(input.env, "HUB_SYSTEMD_UNIT", problems);
  if (serviceUnit !== undefined && !/^cph-hub-[a-z0-9][a-z0-9-]*\.service$/.test(serviceUnit)) {
    problems.push("HUB_SYSTEMD_UNIT must match cph-hub-<instance>.service");
  } else if (serviceUnit !== undefined && serviceUnit !== input.expectedServiceUnit) {
    problems.push(`HUB_SYSTEMD_UNIT must equal installed unit ${input.expectedServiceUnit}`);
  }

  const cphBin = readRequired(input.env, "CPH_BIN", problems);
  if (cphBin !== undefined && !isAbsoluteNormalized(cphBin)) {
    problems.push("CPH_BIN must be an absolute normalized path");
  }

  let bind: ServerBinding | undefined;
  readRequired(input.env, "PORT", problems);
  try {
    bind = readServerBinding(input.env);
  } catch (error) {
    problems.push(error instanceof Error ? error.message : String(error));
  }

  const publicBaseUrl = readRequired(input.env, "HUB_PUBLIC_BASE_URL", problems);
  if (publicBaseUrl !== undefined && !isHttpsUrl(publicBaseUrl)) {
    problems.push("HUB_PUBLIC_BASE_URL must use https");
  }

  const sessionSecret = readRequired(input.env, "HUB_SESSION_SECRET", problems);
  if (sessionSecret !== undefined && sessionSecret.length < 32) {
    problems.push("HUB_SESSION_SECRET must contain at least 32 characters");
  }

  validateOptionalPositiveInteger(input.env, "HUB_AGENT_MAX_TURNS", problems);
  readRequiredPositiveInteger(input.env, "HUB_AGENT_MAX_CONCURRENT_RUNS", problems);
  readRequiredPositiveInteger(input.env, "HUB_AGENT_MAX_RUN_SECONDS", problems);
  readRequiredPositiveInteger(input.env, "HUB_HTTP_BODY_LIMIT_BYTES", problems);
  readRequiredPositiveInteger(input.env, "HUB_MAX_FILES_PER_MESSAGE", problems);
  readRequiredPositiveInteger(input.env, "HUB_MAX_FILE_BYTES", problems);
  readRequiredPositiveInteger(input.env, "HUB_HTTP_REQUESTS_PER_MINUTE", problems);
  readRequiredPositiveInteger(input.env, "HUB_FEISHU_EVENTS_PER_MINUTE", problems);
  validateOptionalBoolean(input.env, "HUB_FEISHU_LISTENER_ENABLED", problems);

  if (problems.length > 0) {
    throw new DeploymentPreflightError(problems);
  }

  return {
    bind: bind!,
    databaseUrl: databaseUrl!,
    cphBin: cphBin!,
  };
}

function readRequired(env: DeploymentEnv, name: string, problems: string[]): string | undefined {
  const value = env[name]?.trim();
  if (value === undefined || value === "") {
    problems.push(`${name} is required`);
    return undefined;
  }
  return value;
}

function validateOptionalPositiveInteger(env: DeploymentEnv, name: string, problems: string[]): void {
  const raw = env[name]?.trim();
  if (raw === undefined || raw === "") return;
  const value = Number(raw);
  if (!Number.isInteger(value) || value <= 0) {
    problems.push(`${name} must be a positive integer`);
  }
}

function readRequiredPositiveInteger(env: DeploymentEnv, name: string, problems: string[]): void {
  const value = readRequired(env, name, problems);
  if (value === undefined) return;
  if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
    problems.push(`${name} must be a positive integer`);
  }
}

function validateOptionalBoolean(env: DeploymentEnv, name: string, problems: string[]): void {
  const raw = env[name]?.trim().toLowerCase();
  if (raw === undefined || raw === "") return;
  if (!["1", "0", "true", "false", "yes", "no", "on", "off"].includes(raw)) {
    problems.push(`${name} must be a boolean value`);
  }
}

function isAbsoluteNormalized(value: string): boolean {
  return isAbsolute(value) && normalize(value) === value;
}

function pathsOverlap(left: string, right: string): boolean {
  return left === right || isStrictDescendant(left, right) || isStrictDescendant(right, left);
}

function isStrictDescendant(parent: string, child: string): boolean {
  const candidate = relative(parent, child);
  return candidate !== "" && candidate !== ".." && !candidate.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(candidate);
}

function isPostgresUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return (url.protocol === "postgresql:" || url.protocol === "postgres:") && url.hostname !== "";
  } catch {
    return false;
  }
}

function isHttpsUrl(value: string): boolean {
  try {
    const url = new URL(value);
    return url.protocol === "https:" && url.hostname !== "" && url.username === "" && url.password === "";
  } catch {
    return false;
  }
}
