/**
 * Alexa channel provider.
 *
 * Unlike WhatsApp, this channel has no socket, no QR, no auth state of its own.
 * It's a degenerate provider whose `sendMessage` resolves a promise held by an
 * inbound HTTP handler. The relay (api.bloby.bot) is the public entry point that
 * verifies Amazon's signature, then forwards the parsed utterance to this Pi
 * via POST /api/channels/alexa/handle. That route waits for the agent's reply
 * and returns it as the HTTP response, which the relay then formats as the
 * Alexa-flavored JSON envelope.
 *
 * The promise-resolution flow is per-conversation FIFO, mirroring the same
 * anti-bleed contract WhatsApp uses: each pushed utterance reserves a resolver
 * slot, each bot:response consumes the head slot.
 */

import { loadConfig } from '../../shared/config.js';
import { log } from '../../shared/logger.js';
import type { ChannelProvider, ChannelStatus, ChannelType } from './types.js';

/** Credentials + state needed to fire Progressive Response for a single Alexa turn. */
export interface AlexaTurnState {
  apiEndpoint: string;
  apiAccessToken: string;
  requestId: string;
  /** Wall-clock start of the turn — used for log timing. */
  startedAt: number;
  /** Static-fallback timer that fires "Working on it" if no preamble text arrives in time. */
  fallbackTimer: ReturnType<typeof setTimeout> | null;
  /** True once we've sent at least one Progressive Response for this turn. */
  sentAny: boolean;
}

interface PendingSlot {
  resolve: (text: string) => void;
  reject: (err: Error) => void;
  createdAt: number;
  turn: AlexaTurnState | null;
}

const STATIC_FALLBACK_MS = 1_500;  // Fire early enough to extend Alexa's budget on cold start
const MAX_PROGRESSIVE_SPEECH = 600;

export class AlexaChannel implements ChannelProvider {
  readonly type: ChannelType = 'alexa';

  /** Per-conversation FIFO of pending HTTP-response resolvers + turn state.
   *  Each inbound Alexa utterance enqueues one slot; each bot:response (or
   *  turn-complete safety net) dequeues one. */
  private pending = new Map<string, PendingSlot[]>();

  /** Reserve a resolver slot. The caller pushes the user utterance into the
   *  live conversation IMMEDIATELY after this returns, so the FIFO order on
   *  this map matches the FIFO order the routing queue uses.
   *
   *  If `creds` are provided (apiEndpoint + apiAccessToken + requestId), this
   *  also schedules a static "Working on it" Progressive Response to fire if
   *  the agent emits no preamble text within STATIC_FALLBACK_MS. The fallback
   *  is cancelled the moment any Progressive Response is sent for this turn. */
  reservePending(
    convId: string,
    creds: { apiEndpoint: string; apiAccessToken: string; requestId: string } | null,
    timeoutMs = 25_000,
  ): Promise<string> {
    return new Promise<string>((resolve, reject) => {
      const turn: AlexaTurnState | null = creds ? {
        apiEndpoint: creds.apiEndpoint,
        apiAccessToken: creds.apiAccessToken,
        requestId: creds.requestId,
        startedAt: Date.now(),
        fallbackTimer: null,
        sentAny: false,
      } : null;

      const slot: PendingSlot = { resolve, reject, createdAt: Date.now(), turn };
      let q = this.pending.get(convId);
      if (!q) {
        q = [];
        this.pending.set(convId, q);
      }
      q.push(slot);

      // NOTE: the relay fires an immediate "On it." Progressive Response on
      // every AgentIntent turn (see relay's sendImmediateProgressive), so the
      // budget is already extended by the time we get here. We DON'T schedule
      // a static fallback on the Pi side — it would double up and the user
      // would hear "On it. Working on it." If the agent emits a preamble, we
      // still flush it as a Progressive in `tryFlushProgressive`.

      // Hard safety timeout — if the agent never responds at all, the HTTP
      // handler unblocks so the relay can return a friendly fallback. The
      // slot is removed so a late bot:response doesn't resolve a dead promise.
      setTimeout(() => {
        const list = this.pending.get(convId);
        if (!list) return;
        const idx = list.indexOf(slot);
        if (idx >= 0) {
          if (slot.turn?.fallbackTimer) clearTimeout(slot.turn.fallbackTimer);
          list.splice(idx, 1);
          if (list.length === 0) this.pending.delete(convId);
          reject(new Error('alexa-timeout'));
        }
      }, timeoutMs);
    });
  }

  /** Resolve the head resolver for a conversation. Called by the channel
   *  manager when bot:response (or turn-complete with no response) fires for
   *  an alexa-surface turn. */
  resolveHead(convId: string, text: string): boolean {
    const q = this.pending.get(convId);
    if (!q || q.length === 0) return false;
    const slot = q.shift()!;
    if (q.length === 0) this.pending.delete(convId);
    if (slot.turn?.fallbackTimer) clearTimeout(slot.turn.fallbackTimer);
    slot.resolve(text);
    return true;
  }

  /** Drop the head resolver (used when a turn ends without a bot:response). */
  rejectHead(convId: string, reason: string): boolean {
    const q = this.pending.get(convId);
    if (!q || q.length === 0) return false;
    const slot = q.shift()!;
    if (q.length === 0) this.pending.delete(convId);
    if (slot.turn?.fallbackTimer) clearTimeout(slot.turn.fallbackTimer);
    slot.reject(new Error(reason));
    return true;
  }

  /** Try to flush a buffered preamble chunk as Progressive Response on the
   *  head turn. Called by the channel manager on bot:tool events when the
   *  routing target's surface is 'alexa' and there's buffered text. */
  tryFlushProgressive(convId: string, text: string): boolean {
    const q = this.pending.get(convId);
    if (!q || q.length === 0) return false;
    const turn = q[0].turn;
    if (!turn) return false;
    this.sendProgressive(turn, text).catch(() => {});
    return true;
  }

  /** Fire a single Progressive Response directive to Amazon's Directive Service.
   *  Best-effort: failures are logged but don't break the agent's stream. */
  private async sendProgressive(turn: AlexaTurnState, speech: string): Promise<void> {
    const trimmed = String(speech || '').trim();
    if (!trimmed) return;
    const fireOffset = Date.now() - turn.startedAt;
    try {
      const r = await fetch(`${turn.apiEndpoint}/v1/directives`, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${turn.apiAccessToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          header: { requestId: turn.requestId },
          directive: {
            type: 'VoicePlayer.Speak',
            speech: trimmed.slice(0, MAX_PROGRESSIVE_SPEECH),
          },
        }),
      });
      if (r.ok) {
        turn.sentAny = true;
        if (turn.fallbackTimer) {
          clearTimeout(turn.fallbackTimer);
          turn.fallbackTimer = null;
        }
        log.info(`[alexa/progressive] sent at +${fireOffset}ms (status ${r.status}) — "${trimmed.slice(0, 60)}"`);
      } else {
        const body = await r.text().catch(() => '');
        log.warn(`[alexa/progressive] REJECTED at +${fireOffset}ms — status ${r.status} body=${body.slice(0, 200)}`);
      }
    } catch (err: any) {
      log.warn(`[alexa/progressive] FAILED at +${fireOffset}ms — ${err.message}`);
    }
  }

  // ── ChannelProvider implementation ──

  async connect(): Promise<void> {
    // No-op. Alexa is a passive HTTP receiver.
    log.info('[alexa] Channel ready (passive HTTP receiver)');
  }

  async disconnect(): Promise<void> {
    // Reject any in-flight resolvers so HTTP handlers don't hang forever,
    // and cancel any pending fallback timers to avoid late progressive calls.
    for (const [, q] of this.pending) {
      for (const slot of q) {
        if (slot.turn?.fallbackTimer) clearTimeout(slot.turn.fallbackTimer);
        slot.reject(new Error('alexa-disconnected'));
      }
    }
    this.pending.clear();
  }

  async sendMessage(_to: string, _text: string): Promise<void> {
    // Alexa is request/response. Replies are delivered by resolving the
    // pending HTTP response, not by calling this method. ChannelManager
    // routes alexa-surface turns through `resolveHead`, not `sendMessage`.
    log.warn('[alexa] sendMessage called on Alexa channel — ignored (use resolveHead)');
  }

  getStatus(): ChannelStatus {
    const cfg = loadConfig().channels?.alexa;
    return {
      channel: 'alexa',
      connected: !!cfg?.enabled,
      info: {
        linked: !!cfg?.sharedSecret,
      },
    };
  }

  getQrCode(): string | null { return null; }
  hasCredentials(): boolean { return !!loadConfig().channels?.alexa?.sharedSecret; }
}
