/**
 * Typed, LOUD errors for the IB Gateway TWS-socket transport (kestrel-7o2.5). A dropped socket,
 * an auth/handshake failure, a lost heartbeat, or a misconfigured session is **never** a silent
 * no-op — it surfaces here (AGENTS: fail-closed; ADR-0034). Secrets/credentials (the account id)
 * are NEVER placed on an error message: any account echoed is redacted at the call site.
 *
 * These are the edge-adapter's own error types; they are distinct classes (not bare `Error`) so a
 * caller can catch a connection loss specifically and reach STAND_DOWN, and can never mistake a
 * transport failure for an unrelated one.
 */

/**
 * Why a live-capable transport degraded / refused — a closed, typed reason a caller can branch on
 * and log. `mode-gate` is the paper-first Kestrel gate (ADR-0034 §a); it is NOT merely an IBKR
 * port/account fact, so it is refused here regardless of which port the gateway listens on.
 *
 * `live-account` is that gate's COMPLEMENT and ADR-0034 §3's promised second barrier: the mode gate
 * guards the mode STRING, which a misconfigured port cannot falsify — `--port 7496` against a
 * live-logged-in gateway is `mode=paper` all the way down while the SOCKET is attached to real
 * money. `live-account` is the handshake ASSERTION that the account we actually reached is a paper
 * one. The mode was never the leak; the socket was.
 */
export type IbkrFailure =
  | "not-connected"
  | "connect-failed"
  | "auth-failed"
  | "heartbeat-lost"
  | "disconnected"
  | "mode-gate"
  | "live-account";

/**
 * Why a CONTRACT could not be resolved (kestrel-7o2.6) — a closed, typed reason the caller branches
 * on and logs before STANDing DOWN. There is deliberately no "best guess" member: the contract layer
 * maps a Kestrel identity onto the gateway's OWN definition or it refuses.
 *
 * - `unsupported`   — the intent is not a legal instrument identity to look up: a HALF-specified
 *   option leg (strike xor right — ADR-0017 requires BOTH or NEITHER), or an option leg with no
 *   supplied expiry. Refused BEFORE the gateway is asked; an expiry/right/strike is never invented.
 * - `unresolvable`  — the gateway has NO definition matching the identity (an unknown strike/right/
 *   expiry, a dead symbol, or a definition that came back incomplete). The wall is the venue's.
 * - `ambiguous`     — the gateway returned MORE THAN ONE matching definition. Never a pick: choosing
 *   one of several (a.m.- vs p.m.-settled, two trading classes, two primary exchanges) would route a
 *   real order to a contract the author did not name.
 * - `lookup-failed` — the definition lookup itself failed (a fatal IB error mid-request, a socket
 *   drop, or a deadline lapse). Distinguished from `unresolvable` because the venue never answered:
 *   the identity may well be fine, so the operator must not go hunting for a bad strike.
 */
export type IbkrContractFailure = "unsupported" | "unresolvable" | "ambiguous" | "lookup-failed";

/**
 * A Kestrel identity could not be mapped onto exactly ONE broker contract (kestrel-7o2.6) — the
 * LOUD, typed, fail-closed refusal that routes to STAND_DOWN. Thrown INSTEAD of returning a guessed
 * contract: an ambiguous, unresolvable, or half-specified identity NEVER yields a contract.
 *
 * Carries the typed {@link IbkrContractFailure} kind, a human-legible {@link reason} (already
 * secret-free — the account is redacted at the call site, as the transport redacts it), the IB
 * {@link code} when the venue named one, and — for `ambiguous` — how many candidates the gateway
 * returned, so the log says *how* ambiguous without ever naming the arbitrary winner (there is none).
 */
export class IbkrContractError extends Error {
  override readonly name = "IbkrContractError";
  readonly failure: IbkrContractFailure;
  /** The secret-free reason, logged verbatim on the STAND_DOWN path. */
  readonly reason: string;
  /** The underlying IB error code, when the venue named one (e.g. 200 "no security definition"). */
  readonly code: number | undefined;
  /** How many definitions the gateway matched. `0` for unresolvable, `>1` for ambiguous. */
  readonly candidates: number | undefined;
  constructor(
    failure: IbkrContractFailure,
    reason: string,
    // `| undefined` is explicit for `exactOptionalPropertyTypes`: a caller forwarding an
    // already-`number | undefined` code/candidate count must still typecheck.
    options: { code?: number | undefined; candidates?: number | undefined; cause?: unknown } = {},
  ) {
    super(
      `IBKR contract resolution refused [${failure}]: ${reason} (fail-closed; STAND_DOWN)`,
      options.cause === undefined ? undefined : { cause: options.cause },
    );
    this.failure = failure;
    this.reason = reason;
    this.code = options.code;
    this.candidates = options.candidates;
  }
}

/** IB Gateway transport configuration is missing or inconsistent (fail-closed; no baked endpoint,
 * so an unresolved host/port surfaces here rather than defaulting silently to a wrong venue). */
export class IbkrConfigError extends Error {
  override readonly name = "IbkrConfigError";
  constructor(message: string, options: { cause?: unknown } = {}) {
    super(message, options.cause === undefined ? undefined : { cause: options.cause });
  }
}

/**
 * The IB Gateway socket connection failed, dropped, lost its heartbeat, or failed to authenticate
 * (kestrel-7o2.5). Thrown on USE of a transport that is not connected or has gone degraded, and
 * used to REJECT a `connect()` that never completed its handshake. Carries the typed
 * {@link IbkrFailure} kind and the underlying IB {@link ErrorCode} (a number) when one is known, so
 * the caller can log the exact wall and STAND_DOWN. NEVER carries a credential/account.
 */
export class IbkrConnectionError extends Error {
  override readonly name = "IbkrConnectionError";
  readonly failure: IbkrFailure;
  /** The underlying IB `ErrorCode` (numeric) when the failure originated from a TWS error event. */
  readonly code: number | undefined;
  constructor(
    failure: IbkrFailure,
    message: string,
    // `| undefined` is explicit: under `exactOptionalPropertyTypes` a caller forwarding an
    // already-`number | undefined` code (a latched failure being rethrown) must still typecheck.
    options: { code?: number | undefined; cause?: unknown } = {},
  ) {
    super(message, options.cause === undefined ? undefined : { cause: options.cause });
    this.failure = failure;
    this.code = options.code;
  }
}
