/**
 * Per-host FIFO action queue.
 *
 * All host-mutating actions (warm, unload) from BooControl serialize through
 * a single FIFO queue per provider_id. Queue discipline:
 *
 * - Submissions rejected immediately while host liveness is 'down'
 * - Queue depth capped at 4; reject-on-full includes pending queue contents
 * - Each action re-checks liveness on dequeue and skips if stale
 * - Unload-during-bench returns 409 {error: 'bench in progress', requiresConfirmation: true}
 *
 * Pattern: arena-runner.ts advanceChain promise-chain + read-fresh-state-or-skip.
 */

import type { FastifyBaseLogger } from 'fastify';

export type ActionType = 'warm' | 'unload';

export interface QueuedAction {
  actionId: string;
  type: ActionType;
  providerId: string;
  model?: string; // for warm: target model; for unload: specific model or undefined for all
  confirmed: boolean; // true if client confirmed takeover
  createdAt: Date;
}

export interface ActionQueueEntry {
  action: QueuedAction;
  status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
  error?: string;
  enqueuedAt: Date;
}

export interface ActionQueueState {
  queue: ActionQueueEntry[];
  running: boolean;
}

export interface ActionQueueDeps {
  baseUrl: string;
  isLivenessUp: () => boolean;
  isInflightRequests: () => number;
  log: FastifyBaseLogger;
}

const MAX_QUEUE_DEPTH = 4;

export class ActionQueue {
  private queues: Map<string, ActionQueueState> = new Map();
  private depsMap: Map<string, ActionQueueDeps> = new Map();

  registerHost(providerId: string, deps: ActionQueueDeps): void {
    this.depsMap.set(providerId, deps);
    if (!this.queues.has(providerId)) {
      this.queues.set(providerId, { queue: [], running: false });
    }
  }

  /**
   * Submit an action to the per-host queue.
   * Returns rejection reasons for: host down, queue full, bench in progress.
   */
  submit(action: QueuedAction): { ok: true } | { ok: false; error: string; pending?: QueuedAction[]; requiresConfirmation?: boolean } {
    const deps = this.depsMap.get(action.providerId);
    if (!deps) {
      return { ok: false, error: `unknown host: ${action.providerId}` };
    }

    // Reject if host is down
    if (!deps.isLivenessUp()) {
      return { ok: false, error: 'host offline' };
    }

    const state = this.queues.get(action.providerId);
    if (!state) {
      return { ok: false, error: `queue not initialized for ${action.providerId}` };
    }

    if (action.type === 'unload' && !action.confirmed) {
      const inflight = deps.isInflightRequests();
      if (inflight > 0) {
        return {
          ok: false,
          error: 'bench in progress',
          requiresConfirmation: true,
        };
      }
    }

    // Depth cap
    if (state.queue.length >= MAX_QUEUE_DEPTH) {
      const pending = state.queue.map((e) => e.action);
      return {
        ok: false,
        error: `queue full (${state.queue.length}/${MAX_QUEUE_DEPTH})`,
        pending,
      };
    }

    const entry: ActionQueueEntry = {
      action,
      status: 'pending',
      enqueuedAt: new Date(),
    };
    state.queue.push(entry);

    // Kick the processor
    void this.processNext(action.providerId, deps);
    return { ok: true };
  }

  /**
   * Get the current queue state for a host.
   */
  getState(providerId: string): ActionQueueState | null {
    return this.queues.get(providerId) ?? null;
  }

  /**
   * Process the next action in the queue for a host.
   * Uses promise-chain pattern: each action runs to completion before the next.
   */
  private async processNext(providerId: string, deps: ActionQueueDeps): Promise<void> {
    const state = this.queues.get(providerId);
    if (!state || state.running || state.queue.length === 0) return;

    state.running = true;
    const entry = state.queue[0];
    if (!entry) {
      state.running = false;
      return;
    }

    entry.status = 'running';

    try {
      // Re-check liveness on dequeue — skip stale actions
      if (!deps.isLivenessUp()) {
        entry.status = 'skipped';
        entry.error = 'host went down during queue wait';
        state.queue.shift();
        state.running = false;
        void this.processNext(providerId, deps);
        return;
      }

      // Re-check if action is still valid (stale warm after model loaded, etc.)
      if (entry.action.type === 'warm' && this.isModelAlreadyLoaded(providerId, entry.action.model)) {
        entry.status = 'skipped';
        entry.error = 'model already loaded';
        state.queue.shift();
        state.running = false;
        void this.processNext(providerId, deps);
        return;
      }

      await this.executeAction(entry.action, deps);
      entry.status = 'completed';
    } catch (err) {
      entry.status = 'failed';
      entry.error = (err as Error).message ?? String(err);
      deps.log.error({ actionId: entry.action.actionId, err: entry.error }, 'action: failed');
    }

    state.queue.shift();
    state.running = false;
    void this.processNext(providerId, deps);
  }

  private async executeAction(action: QueuedAction, deps: ActionQueueDeps): Promise<void> {
    const baseUrl = deps.baseUrl;

    switch (action.type) {
      case 'warm': {
        // 1-token POST /v1/chat/completions with bare wire ID
        if (!action.model) {
          throw new Error('warm action requires model');
        }
        const res = await fetch(`${baseUrl}/v1/chat/completions`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            model: action.model,
            prompt: '.',
            max_tokens: 1,
            stream: false,
          }),
          signal: AbortSignal.timeout(60_000),
        });
        if (!res.ok) {
          const body = await res.text().catch(() => '');
          throw new Error(`warm failed: ${res.status} ${body.slice(0, 200)}`);
        }
        break;
      }

      case 'unload': {
        let url: string;
        if (action.model) {
          url = `${baseUrl}/api/models/unload/${encodeURIComponent(action.model)}`;
        } else {
          url = `${baseUrl}/api/models/unload`;
        }
        const res = await fetch(url, {
          method: 'POST',
          signal: AbortSignal.timeout(30_000),
        });
        if (!res.ok) {
          const body = await res.text().catch(() => '');
          throw new Error(`unload failed: ${res.status} ${body.slice(0, 200)}`);
        }
        break;
      }
    }
  }

  /**
   * Check if a model is already loaded on the host (stale-action guard).
   * This is a placeholder — the real check reads from fleet state.
   */
  private isModelAlreadyLoaded(_providerId: string, _model: string | undefined): boolean {
    // Will be wired to fleet state in index.ts
    return false;
  }

  /**
   * Set the model-loaded check callback (wired from index.ts).
   */
  setModelLoadedCheck(fn: (providerId: string, model: string | undefined) => boolean): void {
    const original = this.isModelAlreadyLoaded.bind(this);
    this.isModelAlreadyLoaded = fn;
  }
}
