import { Injectable, inject, signal } from '@angular/core';
import { FX_QUOTE_GATEWAY } from '../core/pricing/services/fx-quote-gateway.token';
import { FxQuote, isFxQuoteExpired } from '../core/pricing/models/fx-quote.model';

/**
 * Server-sourced currency conversion. Contract:
 * docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3.
 *
 * Previously: admin-typed rates persisted to localStorage, seeded from a
 * hardcoded DEFAULT_RATES table that never updated and drifted from market
 * (USD: 0.011, AMD: 4.3, fixed at whatever date someone last typed them in).
 * That is exactly the class of bug §5 of the contract exists to close - the
 * amount actually charged must be reconstructable from real data, not from
 * a number an admin guessed once.
 *
 * getRate/convert stay synchronous because their call sites (a `pure: false`
 * pipe, cart's computed signals) are synchronous today and rewriting every
 * template into `| async` is a separate, much larger change. The honest
 * tradeoff that follows from that: before a quote has loaded for a pair,
 * getRate returns 1 (same-currency, i.e. "not yet converted") rather than a
 * fabricated market rate - a caller that needs to know whether a rate is
 * real can check isRateReady().
 */
@Injectable({ providedIn: 'root' })
export class CurrencyRatesService {
  private readonly fxQuoteGateway = inject(FX_QUOTE_GATEWAY);

  private readonly quotesSignal = signal<Record<string, FxQuote>>({});
  private readonly inFlight = new Set<string>();
  private readonly versionSignal = signal(0);

  readonly baseCurrency = 'RUB';

  /**
   * Bumped every time a quote is added or refreshed. Quotes now arrive
   * asynchronously and can update a rate for a pair that was already
   * displayed, so anything that memoizes convert()'s output (the
   * `pure: false` currencyConvert pipe caches on (amount, from, to)) needs a
   * signal that changes even when those three inputs are unchanged, or a
   * price rendered before the quote loaded is stuck at rate=1 forever.
   */
  readonly ratesVersion = this.versionSignal.asReadonly();

  /** True once a non-expired quote exists for this pair. */
  readonly isRateReady = (code: string): boolean => {
    if (code === this.baseCurrency) {
      return true;
    }
    const quote = this.quotesSignal()[this.pairKey(code)];
    return quote !== undefined && !isFxQuoteExpired(quote);
  };

  getRate(code: string): number {
    if (code === this.baseCurrency) {
      return 1;
    }

    const key = this.pairKey(code);
    const quote = this.quotesSignal()[key];

    if (!quote || isFxQuoteExpired(quote)) {
      this.fetchQuote(code);
    }

    // A stale-but-present quote is still a real market rate from a moment
    // ago, which is a better estimate than 1 while the refetch is in
    // flight - only "never fetched" falls back to 1.
    return quote?.rate ?? 1;
  }

  /** Converts an amount expressed in `fromCurrency` into `toCurrency` via the RUB base rate. */
  convert(amount: number, fromCurrency: string, toCurrency: string): number {
    if (fromCurrency === toCurrency) {
      return amount;
    }
    const fromRate = this.getRate(fromCurrency);
    const toRate = this.getRate(toCurrency);
    const amountInBase = amount / fromRate;
    return amountInBase * toRate;
  }

  /**
   * Forces a fresh quote for the given currency, ignoring any cached value.
   * Callers that are about to charge money (checkout) should await this
   * before reading a rate, per contract §3.2's stale-quote policy - a rate
   * cached moments ago is fine for display, not for computing a charge.
   */
  async ensureFreshQuote(code: string): Promise<void> {
    if (code === this.baseCurrency) {
      return;
    }
    await this.fetchQuotePromise(code);
  }

  private fetchQuote(code: string): void {
    const key = this.pairKey(code);
    if (this.inFlight.has(key)) {
      return;
    }
    this.inFlight.add(key);

    this.fxQuoteGateway.getQuote(this.baseCurrency, code).subscribe({
      next: quote => {
        this.quotesSignal.update(current => ({ ...current, [key]: quote }));
        this.versionSignal.update(v => v + 1);
        this.inFlight.delete(key);
      },
      error: () => {
        // Leave any previously cached quote in place rather than clearing it -
        // a stale rate that is still roughly right beats no rate at all.
        this.inFlight.delete(key);
      },
    });
  }

  private fetchQuotePromise(code: string): Promise<void> {
    const key = this.pairKey(code);
    // Shares inFlight with fetchQuote() so a concurrent getRate() call for
    // the same pair doesn't fire a second, redundant request.
    this.inFlight.add(key);
    return new Promise(resolve => {
      this.fxQuoteGateway.getQuote(this.baseCurrency, code).subscribe({
        next: quote => {
          this.quotesSignal.update(current => ({ ...current, [key]: quote }));
          this.versionSignal.update(v => v + 1);
          this.inFlight.delete(key);
          resolve();
        },
        error: () => {
          this.inFlight.delete(key);
          resolve();
        },
      });
    });
  }

  private pairKey(quoteCode: string): string {
    return `${this.baseCurrency}_${quoteCode}`;
  }
}
