import type { ChannelAttachmentDto, RoutingDecisionInfo } from '@mosaicstack/types';

/**
 * The single chat execution strategy resolved by {@link ChatRuntimeRouter}.
 *
 * Exactly one runtime is live per process. There is no union that lets a
 * `pi-rpc` deployment silently fall back to embedded execution: an unmet
 * `pi-rpc` precondition is a typed startup failure, never a downgrade.
 */
export type ChatRuntimeMode = 'legacy' | 'pi-rpc';

export type ChatRuntimeKind = 'embedded' | 'harness';

/** The resolved runtime. Slice Zero exposes only its immutable {@link ChatRuntimeKind}. */
export interface ChatRuntime {
  readonly kind: ChatRuntimeKind;
}

/** Why the `pi-rpc` runtime could not be made ready. Both are hard startup failures. */
export type ChatRuntimeUnavailableReason =
  | 'adapter_unavailable'
  | 'conversation_service_unavailable';

/**
 * Raised at module init when `pi-rpc` mode is selected but its preconditions are
 * unmet. Carries only fixed, browser-safe text — never a raw exception message,
 * stack, or provider detail — and reports the frozen ack code `runtime_unsupported`.
 */
export class ChatRuntimeUnavailableError extends Error {
  readonly code = 'runtime_unsupported' as const;
  readonly reason: ChatRuntimeUnavailableReason;

  constructor(reason: ChatRuntimeUnavailableReason) {
    super(
      reason === 'adapter_unavailable'
        ? 'The pi-rpc chat runtime is unavailable: no "pi" harness adapter is registered.'
        : 'The pi-rpc chat runtime is unavailable: the harness conversation service is not bound.',
    );
    this.name = 'ChatRuntimeUnavailableError';
    this.reason = reason;
  }
}

/**
 * Resolves the process-wide chat runtime mode from the environment. Anything other
 * than the exact opt-in token `pi-rpc` keeps the legacy embedded runtime.
 */
export function resolveChatRuntimeMode(
  env: Record<string, string | undefined> = process.env,
): ChatRuntimeMode {
  return env['CHAT_HARNESS_RUNTIME'] === 'pi-rpc' ? 'pi-rpc' : 'legacy';
}

// ---------------------------------------------------------------------------
// Transitional embedded chat port (Task Five).
//
// The legacy embedded browser behaviour is moved behind this exact interface so
// neither the controller nor the gateway retains AgentService, RoutingEngine,
// session, `piSession`, metric, listener, or channel access. `EmbeddedChatRuntime`
// implements the port; `ChatRuntimeRouter` exposes the same narrowly named
// operations and returns `runtime_unsupported` before touching Embedded for legacy
// browser operations when the mode is `pi-rpc`.
//
// The names are frozen (spec jarvis-brain@1c629b06). Legacy REST completion,
// legacy Socket streaming, P3 harness turns, and verified Discord are distinct
// transport/trust capabilities — there is deliberately no generic
// `sendConversationTurn` nor an AgentService-shaped mirror on the router.
// ---------------------------------------------------------------------------

/**
 * Phantom brand keeping {@link OwnedConversationContext} nominally distinct so browser
 * DTOs are never structurally assignable to it. The factory that mints one may be called
 * only after authentication with `scopeFromUser(...)`, never with payload authority fields.
 */
declare const ownedConversationContextBrand: unique symbol;

/** Gateway-only ownership context. Embedded rechecks owner+tenant on every operation. */
export interface OwnedConversationContext {
  readonly [ownedConversationContextBrand]: true;
  readonly conversationId: string;
  readonly scope: Readonly<{ userId: string; tenantId: string }>;
}

/**
 * Every non-`ok` legacy runtime outcome. Missing, foreign, and no-longer-owned
 * conversations all collapse to `conversation_unavailable`. Ownership/mode/validation
 * failures are total results and never throw.
 */
export type LegacyRuntimeFailure =
  | { readonly ok: false; readonly code: 'runtime_unsupported'; readonly retryable: false }
  | { readonly ok: false; readonly code: 'conversation_unavailable'; readonly retryable: false }
  | { readonly ok: false; readonly code: 'request_invalid'; readonly retryable: false }
  | {
      readonly ok: false;
      readonly code: 'thinking_level_invalid';
      readonly retryable: false;
      readonly availableThinkingLevels: readonly string[];
    }
  | { readonly ok: false; readonly code: 'runtime_unavailable'; readonly retryable: true }
  | { readonly ok: false; readonly code: 'turn_already_dispatched'; readonly retryable: false }
  | { readonly ok: false; readonly code: 'operation_failed'; readonly retryable: boolean }
  | { readonly ok: false; readonly code: 'timeout'; readonly retryable: true };

/** Total result: an `ok` value or one of the fixed {@link LegacyRuntimeFailure} codes. */
export type LegacyRuntimeResult<T> =
  | { readonly ok: true; readonly value: T }
  | LegacyRuntimeFailure;

/** User-facing session projection. Carries no session object, handle, or credential path. */
export interface LegacySessionPresentation {
  readonly provider: string;
  readonly modelId: string;
  readonly thinkingLevel: string;
  readonly availableThinkingLevels: readonly string[];
  readonly agentName?: string;
  readonly routingDecision?: RoutingDecisionInfo;
}

/** Terminal usage stats, normalized by Embedded from AgentService metrics. */
export interface LegacyUsage {
  readonly provider: string;
  readonly modelId: string;
  readonly thinkingLevel: string;
  readonly tokens: Readonly<{
    input: number;
    output: number;
    cacheRead: number;
    cacheWrite: number;
    total: number;
  }>;
  readonly cost: number;
  readonly context: Readonly<{ percent: number | null; window: number }>;
}

/**
 * Normalized stream event. Exposes no `AgentSession`, `piSession`, native handle, raw
 * exception, tool arguments, or credential-bearing path — the gateway sees only these.
 */
export type LegacyRuntimeEvent =
  | { readonly type: 'started' }
  | { readonly type: 'text_delta'; readonly text: string }
  | { readonly type: 'thinking_delta'; readonly text: string }
  | {
      readonly type: 'tool_started';
      readonly toolCallId: string;
      readonly toolName: string;
    }
  | {
      readonly type: 'tool_finished';
      readonly toolCallId: string;
      readonly toolName: string;
      readonly isError: boolean;
    }
  | { readonly type: 'settled'; readonly usage?: LegacyUsage };

/** Legacy browser message input. Authority fields are advisory only; scope comes from the context. */
export interface LegacyBrowserMessagePayload {
  readonly content: string;
  readonly provider?: string;
  readonly modelId?: string;
  readonly agentId?: string;
  readonly attachments?: readonly ChannelAttachmentDto[];
}

/** A prepared-but-not-yet-dispatched legacy socket turn. */
export interface LegacySocketTurnLease {
  readonly presentation: LegacySessionPresentation;
  /**
   * Atomically one-shot and scope-rechecking. A second call returns
   * `turn_already_dispatched` and performs zero prompt/tool effects.
   */
  dispatch(): Promise<LegacyRuntimeResult<void>>;
  /** Idempotent, non-throwing. Removes listener and channel, including partial setup. */
  dispose(): Promise<void>;
}

/**
 * Phantom brand for {@link VerifiedDiscordIngressContext}. Minted only after service-token
 * auth plus signature, allowlist, binding, expected-route, replay, configured-agent,
 * forced-scope, and attachment-normalization checks.
 */
declare const verifiedDiscordIngressContextBrand: unique symbol;

/** Fully-verified Discord ingress. Contains no socket, envelope, signature, token, or escape hatch. */
export interface VerifiedDiscordIngressContext {
  readonly [verifiedDiscordIngressContextBrand]: true;
  readonly conversationId: string;
  readonly scope: Readonly<{ userId: string; tenantId: string }>;
  readonly configuredAgent: Readonly<{ agentConfigId: string; instanceId: string }>;
  readonly content: string;
  readonly attachments?: readonly ChannelAttachmentDto[];
  readonly correlationId: string;
  readonly discordMessageId: string;
  readonly discordUserId: string;
}

/** Verified-Discord turn lease. Same atomic one-shot dispatch and idempotent dispose rules. */
export interface VerifiedDiscordTurnLease {
  readonly presentation: LegacySessionPresentation;
  dispatch(): Promise<LegacyRuntimeResult<void>>;
  dispose(): Promise<void>;
}

/** Server-owned egress projection the runtime pushes normalized events into. */
export interface LegacyRuntimeStream {
  /** Server-derived, e.g. `websocket:<socket-id>`. Never client-supplied. */
  readonly channelId: string;
  onEvent(event: LegacyRuntimeEvent): void;
}

/**
 * The exact transitional port. `EmbeddedChatRuntime` implements it; `ChatRuntimeRouter`
 * mirrors the operation names and fails closed with `runtime_unsupported` for legacy
 * browser operations under `pi-rpc`.
 */
export interface LegacyEmbeddedChatPort {
  completeLegacyRestTurn(
    context: OwnedConversationContext,
    input: Readonly<{ content: string }>,
  ): Promise<
    LegacyRuntimeResult<Readonly<{ text: string; presentation: LegacySessionPresentation }>>
  >;

  prepareLegacySocketTurn(
    context: OwnedConversationContext,
    input: LegacyBrowserMessagePayload,
    stream: LegacyRuntimeStream,
  ): Promise<LegacyRuntimeResult<LegacySocketTurnLease>>;

  setLegacyThinking(
    context: OwnedConversationContext,
    level: string,
  ): LegacyRuntimeResult<LegacySessionPresentation>;

  abortLegacyTurn(context: OwnedConversationContext): Promise<LegacyRuntimeResult<void>>;

  applyLegacyModelOverride(
    context: OwnedConversationContext,
    modelId: string,
  ): LegacyRuntimeResult<LegacySessionPresentation>;

  readLegacySessionPresentation(
    context: OwnedConversationContext,
  ): LegacyRuntimeResult<LegacySessionPresentation>;

  dispatchVerifiedDiscordIngress(
    context: VerifiedDiscordIngressContext,
    stream: LegacyRuntimeStream,
  ): Promise<LegacyRuntimeResult<VerifiedDiscordTurnLease>>;
}

/**
 * Mints an {@link OwnedConversationContext} from a server-derived scope. Callers must pass
 * a scope produced by `scopeFromUser(...)` after authentication — never a client-supplied
 * authority field. The brand is phantom, so this is the only way to obtain the branded type.
 */
export function ownConversation(
  conversationId: string,
  scope: Readonly<{ userId: string; tenantId: string }>,
): OwnedConversationContext {
  return { conversationId, scope } as unknown as OwnedConversationContext;
}

/**
 * Mints a {@link VerifiedDiscordIngressContext}. Callers must have already completed every
 * ingress check (service-token auth, signature, allowlist, binding, expected-route, replay,
 * configured-agent, forced-scope, attachment normalization) before calling this.
 */
export function verifyDiscordIngress(
  fields: Omit<VerifiedDiscordIngressContext, typeof verifiedDiscordIngressContextBrand>,
): VerifiedDiscordIngressContext {
  return { ...fields } as unknown as VerifiedDiscordIngressContext;
}
