export interface ServerTlsConfig {
  enabled: boolean;
  certFile: string | null;
  keyFile: string | null;
  minVersion: 'TLSv1.2' | 'TLSv1.3';
  selfSignedDir: string;
  selfSignedHosts: string[];
  httpRedirect: boolean;
  httpRedirectPort: 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;
}

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;
}

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

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 {
  const tlsPartial = (partial?.tls ?? {}) as Partial<ServerTlsConfig>;
  const enabledDefault = opts.freshInstall;
  const tls: ServerTlsConfig = {
    ...SERVER_TLS_DEFAULTS,
    ...tlsPartial,
    enabled: tlsPartial.enabled ?? enabledDefault,
  };
  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 && tls.httpRedirectPort === opts.httpsPort) {
      throw new Error(`server.tls: http_redirect_port (${tls.httpRedirectPort}) 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 };
}
