import { Injectable, Logger } from '@nestjs/common';
import { createHash } from 'crypto';
import { LaravelApiService } from '../../laravel-api/laravel-api.service';

interface CacheEntry<T> {
  value: T;
  expiresAt: number;
}

/**
 * Resolves caller identity/ownership against Laravel (the source of truth)
 * using the caller's OWN bearer token. The BFF never persists end-client data:
 * the in-memory cache holds only pseudonymous uuids for a short TTL, keyed by
 * a sha256 of the token (never the raw token).
 */
@Injectable()
export class LaravelIdentityService {
  private readonly logger = new Logger(LaravelIdentityService.name);
  private readonly cache = new Map<string, CacheEntry<unknown>>();
  private readonly ttlMs = Number(process.env.IDENTITY_CACHE_TTL_MS ?? 60_000);
  private static readonly MAX_ENTRIES = 5_000;

  constructor(private readonly laravelApi: LaravelApiService) {}

  /** "Does this planner token's tenant contain client {uuid}?" — true/false, fail-closed. */
  async canUserAccessClient(
    token: string,
    clientUuid: string,
  ): Promise<boolean> {
    if (!token) return false;
    const key = `access|${this.hash(token)}|${clientUuid}`;
    const cached = this.fromCache<boolean>(key);
    if (cached !== undefined) return cached;
    try {
      const res = await this.laravelApi.makeAuthenticatedRequest<{
        can_access?: boolean;
      }>(token, {
        method: 'GET',
        endpoint: `/v1/user/client/${encodeURIComponent(clientUuid)}/can-access`,
      });
      const ok = res.status === 200 && res.data?.can_access === true;
      if (ok) this.toCache(key, true); // only cache positives; denials/errors retry
      return ok;
    } catch {
      return false; // fail closed; not cached
    }
  }

  /**
   * Pseudonymous identity of a planner token (uuid + stripe_id from /v1/me).
   *
   * Data minimization: only requests pseudonymous fields (uuid, stripe_id) the BFF needs,
   * via the `fields` query parameter. The request is made to the real Laravel route
   * GET /v1/me (UserController@me) with the caller's own bearer token.
   */
  async resolveMe(
    token: string,
  ): Promise<{ uuid?: string; stripe_id?: string } | null> {
    if (!token) return null;
    const key = `me|${this.hash(token)}`;
    const cached = this.fromCache<{ uuid?: string; stripe_id?: string }>(key);
    if (cached !== undefined) return cached;
    try {
      const res = await this.laravelApi.makeAuthenticatedRequest<{
        uuid?: string;
        stripe_id?: string;
      }>(token, {
        method: 'GET',
        endpoint: '/v1/me',
        params: { fields: 'uuid,stripe_id' },
      });
      if (res.status !== 200 || !res.data) return null;
      const me = { uuid: res.data.uuid, stripe_id: res.data.stripe_id };
      this.toCache(key, me);
      return me;
    } catch {
      return null;
    }
  }

  private hash(token: string): string {
    return createHash('sha256').update(token).digest('hex');
  }

  private fromCache<T>(key: string): T | undefined {
    const entry = this.cache.get(key);
    if (!entry) return undefined;
    if (entry.expiresAt < Date.now()) {
      this.cache.delete(key);
      return undefined;
    }
    return entry.value as T;
  }

  private toCache(key: string, value: unknown): void {
    if (this.cache.size >= LaravelIdentityService.MAX_ENTRIES) {
      for (const [k, v] of this.cache) {
        if (v.expiresAt < Date.now()) this.cache.delete(k);
      }
      if (this.cache.size >= LaravelIdentityService.MAX_ENTRIES) {
        const oldest = this.cache.keys().next().value;
        if (oldest) this.cache.delete(oldest);
      }
    }
    this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
  }
}
