import { Injectable } from '@nestjs/common';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Counter } from 'prom-client';
import {
  CircuitBreaker,
  CircuitOpenError,
  type CircuitState,
  LoggerService,
} from '@modules/shared';
import {
  type ISearchRepository,
  type ListingDocument,
  type SearchParams,
  type SearchResult,
} from '../../domain/repositories/search.repository';
import { PostgresSearchRepository } from './postgres-search.repository';
import { TypesenseSearchRepository } from './typesense-search.repository';

export const SEARCH_DEGRADATION_TOTAL = 'search_degradation_total';

/**
 * Resilient search repository that wraps Typesense with a circuit breaker
 * and automatically falls back to PostgreSQL full-text search when
 * Typesense is unavailable.
 *
 * - Normal operation: all searches go through Typesense.
 * - Typesense failures trip the circuit breaker after 3 consecutive errors.
 * - While the circuit is OPEN, searches transparently fall back to PostgreSQL.
 * - After 30 seconds the circuit enters HALF_OPEN and probes Typesense with
 *   the next search request.
 * - Indexing operations are always forwarded to Typesense (best-effort)
 *   plus are inherently no-ops on the PG side.
 */
@Injectable()
export class ResilientSearchRepository implements ISearchRepository {
  private readonly breaker: CircuitBreaker;

  constructor(
    private readonly typesense: TypesenseSearchRepository,
    private readonly postgres: PostgresSearchRepository,
    private readonly logger: LoggerService,
    @InjectMetric(SEARCH_DEGRADATION_TOTAL)
    private readonly degradationCounter: Counter,
  ) {
    this.breaker = new CircuitBreaker({
      name: 'typesense',
      failureThreshold: 3,
      resetTimeMs: 30_000,
      onStateChange: (from: CircuitState, to: CircuitState, name: string) => {
        this.logger.warn(
          `Circuit breaker [${name}] transitioned: ${from} → ${to}`,
          'ResilientSearch',
        );
        if (to === 'OPEN') {
          this.degradationCounter.inc({ service: 'typesense', event: 'circuit_opened' });
        } else if (to === 'CLOSED') {
          this.logger.log(
            `Circuit breaker [${name}] recovered — Typesense is healthy again`,
            'ResilientSearch',
          );
        }
      },
    });
  }

  async search(params: SearchParams): Promise<SearchResult> {
    try {
      return await this.breaker.exec(() => this.typesense.search(params));
    } catch (err) {
      if (err instanceof CircuitOpenError) {
        this.degradationCounter.inc({ service: 'typesense', event: 'fallback_search' });
        this.logger.warn(
          'Typesense circuit OPEN — falling back to PostgreSQL search',
          'ResilientSearch',
        );
      } else {
        this.degradationCounter.inc({ service: 'typesense', event: 'fallback_search' });
        this.logger.warn(
          `Typesense search failed (${(err as Error).message}) — falling back to PostgreSQL`,
          'ResilientSearch',
        );
      }

      return this.postgres.search(params);
    }
  }

  async indexDocument(doc: ListingDocument): Promise<void> {
    try {
      await this.typesense.indexDocument(doc);
    } catch (err) {
      this.breaker.recordFailure();
      this.degradationCounter.inc({ service: 'typesense', event: 'index_failure' });
      this.logger.warn(
        `Typesense indexDocument failed: ${(err as Error).message}`,
        'ResilientSearch',
      );
    }
  }

  async indexDocuments(docs: ListingDocument[]): Promise<void> {
    try {
      await this.typesense.indexDocuments(docs);
    } catch (err) {
      this.breaker.recordFailure();
      this.degradationCounter.inc({ service: 'typesense', event: 'index_failure' });
      this.logger.warn(
        `Typesense indexDocuments failed (${docs.length} docs): ${(err as Error).message}`,
        'ResilientSearch',
      );
    }
  }

  async removeDocument(id: string): Promise<void> {
    try {
      await this.typesense.removeDocument(id);
    } catch (err) {
      this.breaker.recordFailure();
      this.logger.warn(
        `Typesense removeDocument failed for ${id}: ${(err as Error).message}`,
        'ResilientSearch',
      );
    }
  }

  async ensureCollection(): Promise<void> {
    try {
      await this.typesense.ensureCollection();
      this.breaker.recordSuccess();
    } catch (err) {
      this.breaker.recordFailure();
      this.logger.warn(
        `Typesense ensureCollection failed: ${(err as Error).message} — PostgreSQL fallback is available`,
        'ResilientSearch',
      );
    }
  }

  async dropCollection(): Promise<void> {
    try {
      await this.typesense.dropCollection();
    } catch (err) {
      this.logger.warn(
        `Typesense dropCollection failed: ${(err as Error).message}`,
        'ResilientSearch',
      );
    }
  }
}
