import type { StorageProvider } from '@shade/core';
import { ConfigurationError } from '@shade/core';
import type { ObservabilityHook } from '@shade/observability';

/**
 * Shade SDK configuration.
 *
 * All fields have sane defaults except `prekeyServer` which is required
 * for any networked usage.
 */
export interface ShadeConfig {
  /** Required: base URL of the prekey server */
  prekeyServer: string;

  /**
   * Storage backend. Accepts:
   *   - "memory" — in-memory only (lost on restart, good for tests)
   *   - "sqlite:/path/to/file.db" — SQLite backend
   *   - { type: 'postgres', url: 'postgres://...' } — PostgreSQL backend
   *   - { type: 'indexeddb', dbName?: 'my-app' } — browser IndexedDB
   *   - An explicit StorageProvider instance
   *
   * Default: "memory"
   */
  storage?: StorageSpec;

  /**
   * Your address on the prekey server (e.g. "alice@example.com" or "device:abc123").
   * If omitted (undefined), a random UUID is generated and persisted.
   */
  address?: string | undefined;

  /**
   * Auto-replenish configuration. When the one-time prekey stock drops
   * below `min`, the SDK will generate enough to reach `target` and
   * upload them to the prekey server.
   *
   * Default: { min: 5, target: 20, intervalMs: 60_000 }
   * Pass `false` to disable.
   */
  autoReplenish?: { min: number; target: number; intervalMs: number } | false;

  /**
   * Auto identity rotation. Default OFF.
   * Pass an interval string like '7d' or '30d' to enable periodic rotation.
   */
  autoRotate?: false | '1d' | '7d' | '30d' | '90d';

  /**
   * Optional observer configuration. If provided, starts a Shade Observer
   * endpoint for the dashboard.
   */
  observer?: {
    /** Bearer token for the observer. Must be at least 16 chars. */
    token: string;
    /** Port to listen on. If not set, observer is created but not served. */
    port?: number;
    /** Path prefix to mount under (default: "/shade-observer") */
    basePath?: string;
  };

  /**
   * Optional OpenTelemetry observability hook. Build with
   * `withTracer(otelTracer)` from `@shade/observability`. Default: off.
   *
   * The hook propagates to the session manager (encrypt/decrypt spans),
   * transfer engine (upload/download), and `@shade/files` (per-op).
   * `withTracer()` is itself a no-op unless `SHADE_OTEL_ENABLED` is set,
   * so leaving this configured in code stays free in production until
   * the env-var flips it on.
   */
  observability?: ObservabilityHook;

  /**
   * Optional Key-Transparency verifier (V3.12). When set, every
   * `fetchBundle` validates the server's inclusion proof against the
   * pinned `logPublicKey` and feeds the STH into a `LightWitness` for
   * split-view detection.
   *
   * Modes:
   *  - `'observe'`        — verify proofs when present, do not fail when missing.
   *  - `'observe-strict'` — require a proof on every successful and 404 response.
   *
   * Default: KT verification disabled. Set this to enable.
   */
  keyTransparency?: ShadeKTConfig;
}

/** Public configuration shape for `Shade.keyTransparency`. */
export interface ShadeKTConfig {
  mode: 'observe' | 'observe-strict';
  /** Operator's pinned signing public key (32-byte Ed25519) — base64 or raw bytes. */
  logPublicKey: Uint8Array | string;
  /** Reject STHs older than this many ms. Default 24h. */
  maxStaleMs?: number;
  /** Cap on observed-STH cache. Default 1024. */
  witnessMaxStored?: number;
}

/** Acceptable shapes for `ShadeConfig.storage`. */
export type StorageSpec =
  | string
  | StorageProvider
  | { type: 'postgres'; url: string }
  | { type: 'indexeddb'; dbName?: string };

export interface ResolvedConfig {
  prekeyServer: string;
  storage: StorageSpec;
  address?: string | undefined;
  autoReplenish: { min: number; target: number; intervalMs: number } | false;
  autoRotate: false | '1d' | '7d' | '30d' | '90d';
  observer?: {
    token: string;
    port?: number | undefined;
    basePath: string;
  };
  observability?: ObservabilityHook;
  keyTransparency?: ResolvedKTConfig;
}

export interface ResolvedKTConfig {
  mode: 'observe' | 'observe-strict';
  logPublicKey: Uint8Array;
  maxStaleMs: number;
  witnessMaxStored: number;
}

/** Parse and validate a ShadeConfig, resolving defaults and env var overrides */
export function resolveConfig(input: ShadeConfig): ResolvedConfig {
  if (!input.prekeyServer) {
    throw new ConfigurationError('prekeyServer is required');
  }

  const autoReplenish = input.autoReplenish === false
    ? false
    : {
        min: input.autoReplenish?.min ?? 5,
        target: input.autoReplenish?.target ?? 20,
        intervalMs: input.autoReplenish?.intervalMs ?? 60_000,
      };

  const resolved: ResolvedConfig = {
    prekeyServer: input.prekeyServer.replace(/\/$/, ''),
    storage: input.storage ?? 'memory',
    address: input.address,
    autoReplenish,
    autoRotate: input.autoRotate ?? false,
  };

  if (input.observer) {
    if (!input.observer.token || input.observer.token.length < 16) {
      throw new ConfigurationError('observer.token must be at least 16 characters');
    }
    resolved.observer = {
      token: input.observer.token,
      port: input.observer.port,
      basePath: input.observer.basePath ?? '/shade-observer',
    };
  }

  if (input.observability !== undefined) {
    resolved.observability = input.observability;
  }

  if (input.keyTransparency !== undefined) {
    const kt = input.keyTransparency;
    if (kt.mode !== 'observe' && kt.mode !== 'observe-strict') {
      throw new ConfigurationError(
        `keyTransparency.mode must be 'observe' or 'observe-strict'`,
      );
    }
    let logKey: Uint8Array;
    if (typeof kt.logPublicKey === 'string') {
      try {
        logKey = new Uint8Array(Buffer.from(kt.logPublicKey, 'base64'));
      } catch {
        throw new ConfigurationError(
          'keyTransparency.logPublicKey must be base64 or Uint8Array',
        );
      }
    } else {
      logKey = kt.logPublicKey;
    }
    if (logKey.length !== 32) {
      throw new ConfigurationError(
        `keyTransparency.logPublicKey must be 32 bytes (got ${logKey.length})`,
      );
    }
    resolved.keyTransparency = {
      mode: kt.mode,
      logPublicKey: logKey,
      maxStaleMs: kt.maxStaleMs ?? 24 * 60 * 60 * 1000,
      witnessMaxStored: kt.witnessMaxStored ?? 1024,
    };
  }

  return resolved;
}

/** Parse a rotation interval string ('7d', '30d', etc.) into milliseconds */
export function parseRotationInterval(
  interval: '1d' | '7d' | '30d' | '90d',
): number {
  const map: Record<string, number> = {
    '1d': 24 * 60 * 60 * 1000,
    '7d': 7 * 24 * 60 * 60 * 1000,
    '30d': 30 * 24 * 60 * 60 * 1000,
    '90d': 90 * 24 * 60 * 60 * 1000,
  };
  return map[interval]!;
}
