import { Injectable, inject } from '@angular/core';
import { AUTH_BFF_BASE_URL, AUTH_CSRF_COOKIE_NAME } from 'feature-auth';
import { parseSseStream, type SseFrame } from './sse-parser';

/**
 * Thin wrapper around the BFF's `POST /api/ai/chat` SSE endpoint
 * (per [ADR-0024](../../../../../../docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md)).
 *
 * Why native `fetch` and not `HttpClient`? Angular's `HttpClient`
 * buffers the full response by default; the `observe: 'events'`
 * mode emits chunks but loses the streaming semantics we need
 * for token-by-token rendering. Native `fetch().body` is the
 * standards-compliant way to read a `ReadableStream<Uint8Array>`
 * incrementally; the SSE parser under `./sse-parser` does the
 * rest.
 *
 * Native fetch means the auth + CSRF interceptors do NOT run on
 * this call — they only apply to `HttpClient`. The service
 * therefore sets `credentials: 'include'` (so the
 * `__Host-portal_session` cookie travels) and reads + echoes the
 * `__Host-portal_csrf` cookie manually. The cookie is
 * deliberately NOT `HttpOnly` per ADR-0009 §"CSRF defense", so
 * `document.cookie` reads it without a server round-trip.
 *
 * The chatbot state service owns the AbortController and the
 * message buffer; this service stays stateless.
 */
@Injectable({ providedIn: 'root' })
export class ChatbotApiService {
  private readonly bffBaseUrl = inject(AUTH_BFF_BASE_URL);
  private readonly csrfCookieName = inject(AUTH_CSRF_COOKIE_NAME);

  /**
   * Open the chat stream and yield one parsed SSE frame at a time.
   * The async iterable terminates when:
   *   - the server emits `event: done` (the caller stops iterating),
   *   - the network connection closes,
   *   - `signal.abort()` is invoked (yields no further frames; the
   *     underlying fetch is cancelled).
   *
   * Throws on a non-OK HTTP status, on a missing response body, or
   * on a CSRF cookie that cannot be read — the latter only happens
   * when the session is gone, in which case the SPA should redirect
   * to login per the auth service's contract.
   */
  async openChatStream(
    body: ChatRelayRequestBody,
    signal: AbortSignal,
  ): Promise<AsyncIterable<SseFrame>> {
    const csrfToken = readCookie(this.csrfCookieName);
    if (csrfToken === null) {
      throw new ChatbotApiError(
        'csrf-cookie-missing',
        'No CSRF token available — the portal session has expired or never started.',
      );
    }

    const response = await fetch(`${this.bffBaseUrl}/ai/chat`, {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'text/event-stream',
        'X-CSRF-Token': csrfToken,
      },
      body: JSON.stringify(body),
      signal,
    });

    if (!response.ok) {
      const code =
        response.status === 401
          ? 'unauthenticated'
          : response.status === 403
            ? 'forbidden'
            : 'http-error';
      throw new ChatbotApiError(
        code,
        `The chat endpoint returned HTTP ${response.status}.`,
        response.status,
      );
    }

    if (response.body === null) {
      throw new ChatbotApiError('no-body', 'The chat endpoint returned an empty response body.');
    }

    return parseSseStream(response.body);
  }
}

/** Mirrors the BFF's `ChatRequestDto`. Kept narrow on purpose. */
export interface ChatRelayRequestBody {
  readonly messages: ReadonlyArray<{
    readonly role: 'user' | 'assistant' | 'system';
    readonly content: string;
  }>;
  readonly conversationId?: string;
  readonly model?: string;
  readonly provider?: string;
}

export class ChatbotApiError extends Error {
  constructor(
    readonly code:
      | 'csrf-cookie-missing'
      | 'unauthenticated'
      | 'forbidden'
      | 'http-error'
      | 'no-body',
    message: string,
    readonly httpStatus?: number,
  ) {
    super(message);
    this.name = 'ChatbotApiError';
  }
}

/**
 * Read a cookie from `document.cookie` without depending on a
 * library. Returns `null` when the cookie is absent.
 *
 * The CSRF cookie is set with `HttpOnly: false` on purpose
 * (per ADR-0009 §"CSRF defense") so the SPA can mirror its value
 * into the request header. This helper is the only `document.cookie`
 * read in the chatbot scope.
 */
function readCookie(name: string): string | null {
  if (typeof document === 'undefined' || typeof document.cookie !== 'string') {
    return null;
  }
  const encoded = encodeURIComponent(name);
  for (const pair of document.cookie.split(';')) {
    const trimmed = pair.trim();
    if (trimmed.startsWith(`${encoded}=`)) {
      return decodeURIComponent(trimmed.slice(encoded.length + 1));
    }
    if (trimmed.startsWith(`${name}=`)) {
      return decodeURIComponent(trimmed.slice(name.length + 1));
    }
  }
  return null;
}
