export interface ServerTlsConfig {
  enabled: boolean;
  certFile: string | null;
  keyFile: string | null;
  minVersion: 'TLSv1.2' | 'TLSv1.3';
  selfSignedDir: string;
  selfSignedHosts: string[];
  httpRedirect: boolean;
  /**
   * Ports the HTTP→HTTPS redirector listens on, resolved to a non-empty,
   * de-duplicated array. Config accepts `http_redirect_port` as either a single
   * number (e.g. `9080`) or a list (e.g. `[80, 9876]`); both map here.
   */
  httpRedirectPorts: number[];
  /**
   * Host used to build the HTTP→HTTPS redirect `Location` header.
   * When null the bind host is used instead.
   * Intentionally NOT taken from the request Host header to prevent open-redirect attacks.
   */
  redirectHost: string | null;
  /**
   * Emit `Strict-Transport-Security` (HSTS) on TLS responses. Default OFF.
   *
   * HSTS is deliberately opt-in: it pins a browser to HTTPS for its whole
   * max-age, and the default deployment uses a self-signed cert where that
   * pin is pure footgun — once set, turning HTTPS back off in the UI cannot
   * un-stick the browser (it upgrades http→https before any request reaches
   * the server). When this is false AND TLS is on, the server actively sends
   * `max-age=0` to CLEAR any previously-pinned HSTS so such a browser recovers.
   * Turn this on only behind a real (operator-provided) certificate.
   */
  hsts: boolean;
}

export interface ServerConfig {
  /** HTTP(S) listen port. Resolved by mergeServerConfig (default 9876). */
  port: number;
  tls: ServerTlsConfig;
}

/** Default HTTP(S) listen port when neither PORT nor server.port is set. */
export const DEFAULT_SERVER_PORT = 9876;

/**
 * Resolve the effective listen port from the precedence
 * `PORT env > server.port (config) > default`. Invalid values (non-integer or
 * out of the 1–65535 range) are ignored with a caller-supplied warn hook so a
 * typo can't bind port 0 / NaN. Pure: the env string is passed in.
 */
export function resolveListenPort(
  envPort: string | undefined,
  configPort: number | string | undefined,
  warn?: (msg: string) => void,
): number {
  const isValid = (n: unknown): n is number =>
    typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 65535;

  if (envPort !== undefined && envPort !== '') {
    const n = Number(envPort);
    if (isValid(n)) return n;
    warn?.(`[server] ignoring invalid PORT env "${envPort}" (want an integer 1–65535)`);
  }
  if (configPort !== undefined && configPort !== '') {
    // Accept a numeric YAML value or a quoted "9876" string symmetrically with env.
    const n = typeof configPort === 'string' ? Number(configPort) : configPort;
    if (isValid(n)) return n;
    warn?.(`[server] ignoring invalid server.port "${configPort}" (want an integer 1–65535)`);
  }
  return DEFAULT_SERVER_PORT;
}

/** Default HTTP→HTTPS redirect port when none is configured. */
export const DEFAULT_HTTP_REDIRECT_PORTS = [9080];

export const SERVER_TLS_DEFAULTS: ServerTlsConfig = {
  enabled: false,
  certFile: null,
  keyFile: null,
  minVersion: 'TLSv1.2',
  selfSignedDir: './data/tls',
  selfSignedHosts: [],
  httpRedirect: true,
  httpRedirectPorts: [...DEFAULT_HTTP_REDIRECT_PORTS],
  redirectHost: null,
  hsts: false,
};

/**
 * Coerce a raw `http_redirect_port` / `http_redirect_ports` value into a
 * non-empty, de-duplicated array of valid ports. Accepts a single number, an
 * array, or numeric strings. Invalid/out-of-range entries are dropped; if
 * nothing valid remains, falls back to the default.
 */
export function normalizeRedirectPorts(input: unknown): number[] {
  const arr = Array.isArray(input) ? input : input == null ? [] : [input];
  const valid = arr
    .map((v) => (typeof v === 'string' ? Number(v) : v))
    .filter((n): n is number => typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 65535);
  const unique = [...new Set(valid)];
  return unique.length > 0 ? unique : [...DEFAULT_HTTP_REDIRECT_PORTS];
}

export interface MergeServerOpts {
  freshInstall: boolean;
  /**
   * The already-resolved effective listen port (PORT env > server.port >
   * default), threaded in by the caller. Becomes the merged `port` and is the
   * value the http_redirect_port collision check compares against.
   */
  httpsPort?: number;
}

export function mergeServerConfig(
  partial: Partial<ServerConfig> | undefined,
  opts: MergeServerOpts,
): ServerConfig {
  // Accept the legacy scalar `http_redirect_port` alongside the resolved
  // `http_redirect_ports` array — both arrive untyped from YAML/JSON.
  const tlsPartial = (partial?.tls ?? {}) as Partial<ServerTlsConfig> & {
    httpRedirectPort?: number | number[];
  };
  const enabledDefault = opts.freshInstall;
  const tls: ServerTlsConfig = {
    ...SERVER_TLS_DEFAULTS,
    ...tlsPartial,
    enabled: tlsPartial.enabled ?? enabledDefault,
    httpRedirectPorts: normalizeRedirectPorts(tlsPartial.httpRedirectPorts ?? tlsPartial.httpRedirectPort),
  };
  // Drop the legacy scalar so the resolved object exposes only httpRedirectPorts.
  delete (tls as unknown as Record<string, unknown>)['httpRedirectPort'];
  tls.selfSignedHosts = [...(tlsPartial.selfSignedHosts ?? SERVER_TLS_DEFAULTS.selfSignedHosts)];

  if (tls.enabled) {
    const hasCert = !!tls.certFile;
    const hasKey = !!tls.keyFile;
    if (hasCert !== hasKey) {
      throw new Error('server.tls: set both cert_file and key_file, or neither');
    }
    if (opts.httpsPort != null && tls.httpRedirect) {
      const clash = tls.httpRedirectPorts.find((p) => p === opts.httpsPort);
      if (clash != null) {
        throw new Error(`server.tls: http_redirect_port (${clash}) must differ from the HTTPS port`);
      }
    }
  }
  // opts.httpsPort is the already-resolved listen port in production; the
  // fallback validates/coerces a raw config value for callers that don't pass it.
  const port = opts.httpsPort ?? resolveListenPort(undefined, partial?.port);
  return { port, tls };
}
