/**
 * @fileoverview Legislative sub-client for European Parliament API
 *
 * Handles legislative procedures, procedure events, and adopted texts.
 *
 * @module clients/ep/legislativeClient
 */

import type {
  Procedure,
  AdoptedText,
  EPEvent,
  PaginatedResponse,
} from '../../types/europeanParliament.js';
import {
  transformProcedure as _transformProcedure,
  transformAdoptedText as _transformAdoptedText,
  transformEvent as _transformEvent,
} from './transformers.js';
import {
  BaseEPClient,
  APIError,
  type EPClientConfig,
  type EPSharedResources,
  type JSONLDResponse,
} from './baseClient.js';
import { DEFAULT_TIMEOUTS } from '../../utils/timeout.js';

/**
 * Sub-client for legislative procedures and adopted-texts EP API endpoints.
 *
 * @extends BaseEPClient
 * @public
 */
export class LegislativeClient extends BaseEPClient {
  constructor(config: EPClientConfig = {}, shared?: EPSharedResources) {
    super(config, shared);
  }

  private transformProcedure(apiData: Record<string, unknown>): Procedure {
    return _transformProcedure(apiData);
  }

  private transformAdoptedText(apiData: Record<string, unknown>): AdoptedText {
    return _transformAdoptedText(apiData);
  }

  private transformEvent(apiData: Record<string, unknown>): EPEvent {
    return _transformEvent(apiData);
  }

  /**
   * Returns legislative procedures.
   * **EP API Endpoint:** `GET /procedures`
   *
   * **Note:** The EP API `/procedures` endpoint does **not** support a
   * `year` query parameter per the OpenAPI spec — it only has
   * `process-type`.  Callers needing year-specific counts must filter
   * client-side.
   *
   * @param params - limit, offset
   * @returns Paginated list of procedures
   */
  async getProcedures(params: {
    limit?: number;
    offset?: number;
    abortSignal?: AbortSignal;
  } = {}): Promise<PaginatedResponse<Procedure>> {
    const limit = params.limit ?? 50;
    const offset = params.offset ?? 0;
    const apiParams: Record<string, unknown> = {
      format: 'application/ld+json',
      offset,
      limit,
    };

    const response = await this.get<JSONLDResponse>('procedures', apiParams, undefined, params.abortSignal);
    const items = Array.isArray(response.data) ? response.data : [];
    const procedures = items.map((item) => this.transformProcedure(item));
    const hasMore = procedures.length === limit;
    return { data: procedures, total: procedures.length + offset + (hasMore ? 1 : 0), limit, offset, hasMore };
  }

  /**
   * Returns a single procedure by ID.
   *
   * The EP API wraps single-item responses in a JSON-LD `data` array,
   * so this method extracts `data[0]` before transforming.
   *
   * **EP API Endpoint:** `GET /procedures/{process-id}`
   *
   * @param processId - Procedure **process-id** in `"YYYY-NNNN"` format (e.g. `"2024-0006"`).
   *   This is different from the human-readable `Procedure.id` (`"COD/YYYY/NNNN"`) or
   *   `Procedure.reference` (`"YYYY/NNNN(COD)"`) fields.
   * @throws {APIError} When the procedure is not found (404)
   */
  async getProcedureById(processId: string, options: { abortSignal?: AbortSignal } = {}): Promise<Procedure> {
    if (processId.trim() === '') {
      throw new APIError('Procedure process-id is required', 400);
    }
    const response = await this.get<Record<string, unknown>>(
      `procedures/${processId}`,
      { format: 'application/ld+json' },
      undefined,
      options.abortSignal,
    );
    const dataArray = response['data'];
    if (Array.isArray(dataArray) && dataArray.length > 0) {
      return this.transformProcedure(dataArray[0] as Record<string, unknown>);
    }
    return this.transformProcedure(response);
  }

  /**
   * Returns events linked to a procedure.
   * **EP API Endpoint:** `GET /procedures/{process-id}/events`
   *
   * @param processId - Procedure process ID
   * @param params - limit, offset
   */
  async getProcedureEvents(
    processId: string,
    params: { limit?: number; offset?: number; abortSignal?: AbortSignal } = {}
  ): Promise<PaginatedResponse<EPEvent>> {
    if (processId.trim() === '') {
      throw new APIError('Procedure process-id is required', 400);
    }
    const limit = params.limit ?? 50;
    const offset = params.offset ?? 0;

    const response = await this.get<JSONLDResponse>(
      `procedures/${processId}/events`,
      { format: 'application/ld+json', offset, limit },
      undefined,
      params.abortSignal,
    );

    const items = Array.isArray(response.data) ? response.data : [];
    const events = items.map((item) => this.transformEvent(item));
    const hasMore = events.length === limit;
    return { data: events, total: events.length + offset + (hasMore ? 1 : 0), limit, offset, hasMore };
  }

  /**
   * Returns adopted texts.
   * **EP API Endpoint:** `GET /adopted-texts`
   *
   * @param params - year, limit, offset
   */
  async getAdoptedTexts(params: {
    year?: number;
    limit?: number;
    offset?: number;
    abortSignal?: AbortSignal;
  } = {}): Promise<PaginatedResponse<AdoptedText>> {
    const limit = params.limit ?? 50;
    const offset = params.offset ?? 0;
    const apiParams: Record<string, unknown> = {
      format: 'application/ld+json',
      offset,
      limit,
    };
    if (params.year !== undefined) apiParams['year'] = params.year;

    const response = await this.get<JSONLDResponse>('adopted-texts', apiParams, undefined, params.abortSignal);
    const items = Array.isArray(response.data) ? response.data : [];
    const texts = items.map((item) => this.transformAdoptedText(item));
    const hasMore = texts.length === limit;
    return { data: texts, total: texts.length + offset + (hasMore ? 1 : 0), limit, offset, hasMore };
  }

  /**
   * Retrieves recently updated procedures via the feed endpoint.
   * **EP API Endpoint:** `GET /procedures/feed`
   *
   * **Note:** The EP API `procedures/feed` endpoint is significantly slower
   * than other feed endpoints — it typically takes 25–40 s even for `one-week`
   * and 120+ seconds for `one-month`.  An extended minimum timeout of 120 s
   * is always applied.
   */
  async getProceduresFeed(params: {
    timeframe?: string;
    startDate?: string;
    processType?: string;
    abortSignal?: AbortSignal;
  } = {}): Promise<JSONLDResponse> {
    return this.get<JSONLDResponse>('procedures/feed', {
      format: 'application/ld+json',
      ...(params.timeframe !== undefined ? { timeframe: params.timeframe } : {}),
      ...(params.startDate !== undefined ? { 'start-date': params.startDate } : {}),
      ...(params.processType !== undefined ? { 'process-type': params.processType } : {}),
    }, DEFAULT_TIMEOUTS.EP_FEED_SLOW_REQUEST_MS, params.abortSignal);
  }

  /**
   * Retrieves recently updated adopted texts via the feed endpoint.
   * **EP API Endpoint:** `GET /adopted-texts/feed`
   *
   * Configurable-window feed.  Extended timeout applied for `one-month`.
   */
  async getAdoptedTextsFeed(params: {
    timeframe?: string;
    startDate?: string;
    workType?: string;
    abortSignal?: AbortSignal;
  } = {}): Promise<JSONLDResponse> {
    const minimumTimeout = params.timeframe === 'one-month'
      ? DEFAULT_TIMEOUTS.EP_FEED_SLOW_REQUEST_MS
      : undefined;
    return this.get<JSONLDResponse>('adopted-texts/feed', {
      format: 'application/ld+json',
      ...(params.timeframe !== undefined ? { timeframe: params.timeframe } : {}),
      ...(params.startDate !== undefined ? { 'start-date': params.startDate } : {}),
      ...(params.workType !== undefined ? { 'work-type': params.workType } : {}),
    }, minimumTimeout, params.abortSignal);
  }

  /**
   * Returns a single event within a procedure by event ID.
   * **EP API Endpoint:** `GET /procedures/{process-id}/events/{event-id}`
   *
   * @param processId - Procedure process ID
   * @param eventId - Event identifier within the procedure
   */
  async getProcedureEventById(
    processId: string,
    eventId: string,
    options: { abortSignal?: AbortSignal } = {},
  ): Promise<EPEvent> {
    if (processId.trim() === '') {
      throw new APIError('Procedure process-id is required', 400);
    }
    if (eventId.trim() === '') {
      throw new APIError('Event ID is required', 400);
    }
    const response = await this.get<JSONLDResponse>(
      `procedures/${processId}/events/${eventId}`,
      { format: 'application/ld+json' },
      undefined,
      options.abortSignal,
    );
    if (Array.isArray(response.data)) {
      if (response.data.length === 0) {
        throw new APIError(
          `Procedure event not found for process-id "${processId}" and event ID "${eventId}"`,
          404
        );
      }
      return this.transformEvent(response.data[0] ?? {});
    }

    return this.transformEvent(response);
  }

  /**
   * Returns a single adopted text by document ID.
   * **EP API Endpoint:** `GET /adopted-texts/{doc-id}`
   *
   * **Content-pending detection:** The EP Open Data Portal will sometimes respond
   * with HTTP 200 for a document that is indexed in the feed but whose detail
   * enrichment has not yet completed — every transformable field comes back empty.
   * Returning that shape to callers would emit a response that passes JSON-schema
   * validation but carries no data, leading to blank titles/dates/references being
   * rendered downstream. We treat this sentinel as a 404 so callers get the same
   * error semantics they would for a truly missing document, and we also evict
   * the cached empty payload so availability recovers as soon as the upstream
   * document is enriched (instead of blocking for the full cache TTL).
   *
   * @throws {APIError} 400 when `docId` is empty or whitespace.
   * @throws {APIError} 404 in two cases:
   *   - the upstream returns HTTP 404 because no document exists for `docId`
   *     (re-thrown by {@link BaseEPClient.get}); or
   *   - the upstream returns HTTP 200 but every transformable field is empty
   *     (the indexed-but-content-pending sentinel detected here).
   */
  async getAdoptedTextById(docId: string, options: { abortSignal?: AbortSignal } = {}): Promise<AdoptedText> {
    if (docId.trim() === '') throw new APIError('Document ID is required', 400);
    const endpoint = `adopted-texts/${docId}`;
    const params = { format: 'application/ld+json' };
    const response = await this.get<Record<string, unknown>>(endpoint, params, undefined, options.abortSignal);
    const transformed = this.transformAdoptedText(response);
    if (isEmptyAdoptedText(transformed)) {
      this.evictFromCache(endpoint, params);
      throw new APIError(
        `Adopted text "${docId}": document indexed but content not yet available`,
        404
      );
    }
    return transformed;
  }
}

/**
 * Returns `true` when every string field of an {@link AdoptedText} is empty —
 * the upstream content-pending sentinel that must not be surfaced to callers.
 *
 * Iterates over `Object.values` so the check stays correct if new fields are
 * added to {@link AdoptedText} (all current fields are `string`).
 *
 * @internal
 */
function isEmptyAdoptedText(text: AdoptedText): boolean {
  return Object.values(text).every((v) => typeof v === 'string' && v === '');
}
