import {
  BadGatewayException,
  BadRequestException,
  ConflictException,
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios, {
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
  isAxiosError,
} from 'axios';

import type { ImpersonatedRequest } from './dto/impersonated-request.dto';
import type { ResolveUuidsByEmailsResponse } from './dto/resolve-uuids-by-emails.dto';
import {
  fromLaravelPaginator,
  LaravelMasterUserRow,
  mapLaravelMasterUser,
  PaginatedResponse,
} from './dto/paginated-response.dto';
import type { ListMasterUsersParams, MasterUser } from './dto/master-user.dto';
import type { LookupUserResponse } from './dto/lookup-user.dto';
import {
  AdvisorUsageMetricsResponse,
  LaravelCapabilitiesUsageResponse,
  mapLaravelCapabilitiesUsage,
} from './dto/advisor-usage-metrics.dto';
import { isPlanFiUuidLike, planFiUuidVariants } from './planfi-uuid';
import type {
  BatchFetchUsersResponse,
  BatchFetchByUuidResult,
  BatchFetchByEmailResult,
} from './dto/batch-fetch-users.dto';

const TOKEN_REFRESH_MARGIN_MS = 60_000;
const MOCK_TEST_TOKEN = 'pfm_mock_test_token';

interface MasterTokenCache {
  token: string;
  expiresAt: number;
  allowedMethods: string[];
}

@Injectable()
export class MasterApiService {
  private readonly logger = new Logger(MasterApiService.name);
  private readonly http: AxiosInstance;
  private tokenCache: MasterTokenCache | null = null;
  private inflightTokenPromise: Promise<MasterTokenCache> | null = null;

  constructor(private readonly configService: ConfigService) {
    const baseURL =
      this.configService.get<string>('LARAVEL_URL') || 'http://localhost:8000';

    this.http = axios.create({
      baseURL,
      timeout: 30_000,
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
    });

    this.http.interceptors.request.use(
      (config) => {
        this.logger.debug(
          `${config.method?.toUpperCase()} ${config.baseURL ?? ''}${config.url ?? ''}`,
        );
        return config;
      },
      (error: unknown) => Promise.reject(error),
    );

    this.http.interceptors.response.use(
      (response) => response,
      (error: unknown) => Promise.reject(error),
    );
  }

  async listUsers(
    params: ListMasterUsersParams = {},
  ): Promise<PaginatedResponse<MasterUser>> {
    const query = this.buildUsersQuery(params);
    const payload = await this.executeWithBearerAuth<LaravelPaginatedRaw>({
      method: 'GET',
      url: '/v1/master/users',
      params: query,
    });

    return fromLaravelPaginator<LaravelMasterUserRow, MasterUser>(
      payload,
      mapLaravelMasterUser,
    );
  }

  async searchUsers(
    params: ListMasterUsersParams = {},
  ): Promise<{ data: MasterUser[] }> {
    const result = await this.listUsers(params);
    return { data: result.data };
  }

  async findUserByUuid(uuid: string): Promise<MasterUser | null> {
    const res = await this.listUsers({ uuid, perPage: 1 });
    return res.data[0] ?? null;
  }

  async searchUsersByEmail(email: string): Promise<MasterUser[]> {
    const res = await this.listUsers({ email, perPage: 100 });
    return res.data;
  }

  async lookupUser(params: {
    uuid?: string;
    email?: string;
  }): Promise<LookupUserResponse> {
    const uuid = params.uuid?.trim();
    const email = params.email?.trim().toLowerCase();

    if (!!uuid === !!email) {
      throw new BadRequestException('Provide exactly one of uuid or email');
    }

    if (uuid) {
      if (!isPlanFiUuidLike(uuid)) {
        throw new BadRequestException('Invalid user UUID');
      }

      for (const candidate of planFiUuidVariants(uuid)) {
        const user = await this.findUserByUuid(candidate);
        if (user) {
          return { user };
        }
      }

      throw new NotFoundException('User not found');
    }

    const match = await this.classifyEmailMatches(email!);
    if (match.kind === 'resolved') {
      const user = await this.findUserByUuid(match.uuid);
      if (!user) {
        throw new NotFoundException('User not found');
      }
      return { user };
    }
    if (match.kind === 'ambiguous') {
      throw new ConflictException('Multiple users match this email');
    }
    throw new NotFoundException('User not found');
  }

  async getAdvisorUsageMetrics(
    userUuid: string,
  ): Promise<AdvisorUsageMetricsResponse> {
    const resolvedUuid = await this.resolveImpersonationUserUuid(userUuid);

    const payload =
      await this.makeImpersonatedRequest<LaravelCapabilitiesUsageResponse>(
        resolvedUuid,
        {
          method: 'GET',
          path: '/v1/user/metrics/capabilities-usage',
        },
      );

    return { metrics: mapLaravelCapabilitiesUsage(payload) };
  }

  /** Resolve o UUID canônico do assessor (formato exato no banco PlanFi). */
  private async resolveImpersonationUserUuid(
    userUuid: string,
  ): Promise<string> {
    if (!isPlanFiUuidLike(userUuid)) {
      throw new BadRequestException('Invalid impersonation user UUID');
    }

    for (const candidate of planFiUuidVariants(userUuid)) {
      const user = await this.findUserByUuid(candidate);
      if (user) {
        return user.uuid;
      }
    }

    throw new NotFoundException('User not found');
  }

  /**
   * Resolve `users.uuid` na API Laravel para cada email (match exato; o upstream usa LIKE,
   * portanto aqui filtra-se igualdade após lowercase).
   */
  async resolveUuidsByEmails(
    emails: string[],
  ): Promise<ResolveUuidsByEmailsResponse> {
    const normalizedOrder: string[] = [];
    const seen = new Set<string>();
    for (const raw of emails) {
      const n = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
      if (!n || seen.has(n)) continue;
      seen.add(n);
      normalizedOrder.push(n);
    }

    const CONCURRENCY = 5;

    const perEmailResults = await MasterApiService.runConcurrent(
      normalizedOrder,
      CONCURRENCY,
      async (requestedNorm) => this.classifyEmailMatches(requestedNorm),
    );

    const resolved: ResolveUuidsByEmailsResponse['resolved'] = [];
    const notFound: string[] = [];
    const ambiguous: string[] = [];

    for (const r of perEmailResults) {
      if (r.kind === 'resolved')
        resolved.push({ email: r.email, uuid: r.uuid });
      else if (r.kind === 'notFound') notFound.push(r.email);
      else ambiguous.push(r.email);
    }

    return { resolved, notFound, ambiguous };
  }

  async batchFetchUsers(
    uuids: string[],
    emails: string[],
  ): Promise<BatchFetchUsersResponse> {
    const foundByUuid: BatchFetchByUuidResult[] = [];
    const notFoundUuids: string[] = [];
    const foundByEmail: BatchFetchByEmailResult[] = [];
    const notFoundEmails: string[] = [];

    await MasterApiService.runConcurrent(uuids, 5, async (uuid) => {
      try {
        const { user } = await this.lookupUser({ uuid });
        foundByUuid.push({ requestedUuid: uuid, user });
      } catch {
        notFoundUuids.push(uuid);
      }
    });

    await MasterApiService.runConcurrent(emails, 5, async (email) => {
      try {
        const { user } = await this.lookupUser({ email });
        foundByEmail.push({ requestedEmail: email, user });
      } catch {
        notFoundEmails.push(email);
      }
    });

    return { foundByUuid, foundByEmail, notFoundUuids, notFoundEmails };
  }

  private static async runConcurrent<T, R>(
    items: T[],
    concurrency: number,
    worker: (item: T) => Promise<R>,
  ): Promise<R[]> {
    const out: R[] = new Array(items.length);
    let cursor = 0;
    async function runWorker(): Promise<void> {
      for (;;) {
        const i = cursor++;
        if (i >= items.length) return;
        out[i] = await worker(items[i]);
      }
    }
    const starters =
      items.length === 0 ? 0 : Math.min(concurrency, items.length);
    await Promise.all(Array.from({ length: starters }, () => runWorker()));
    return out;
  }

  private async classifyEmailMatches(
    requestedNorm: string,
  ): Promise<
    | { kind: 'resolved'; email: string; uuid: string }
    | { kind: 'notFound'; email: string }
    | { kind: 'ambiguous'; email: string }
  > {
    const hits = await this.searchUsersByEmail(requestedNorm);
    const exact = hits.filter(
      (u) => u.email.trim().toLowerCase() === requestedNorm,
    );
    if (exact.length === 1) {
      return {
        kind: 'resolved',
        email: requestedNorm,
        uuid: exact[0].uuid,
      };
    }
    if (exact.length === 0) {
      return { kind: 'notFound', email: requestedNorm };
    }
    return { kind: 'ambiguous', email: requestedNorm };
  }

  async makeImpersonatedRequest<T>(
    userUuid: string,
    request: ImpersonatedRequest,
  ): Promise<T> {
    if (!isPlanFiUuidLike(userUuid)) {
      throw new BadRequestException('Invalid impersonation user UUID');
    }
    if (!request.path.startsWith('/')) {
      throw new BadRequestException(
        'Impersonated path must be absolute starting with "/"',
      );
    }

    const impersonationHeader = this.configService.get<string>(
      'MASTER_API_IMPERSONATION_HEADER',
    )!;

    return this.executeWithBearerAuth<T>({
      method: request.method,
      url: request.path,
      params: request.query,
      data: request.body,
      headers: {
        ...request.headers,
        [impersonationHeader]: userUuid,
      },
    });
  }

  private buildUsersQuery(
    params: ListMasterUsersParams,
  ): Record<string, string | number | boolean> {
    const q: Record<string, string | number | boolean> = {};
    if (params.perPage !== undefined) q.per_page = params.perPage;
    if (params.uuid !== undefined) q.uuid = params.uuid;
    if (params.email !== undefined) q.email = params.email;
    if (params.search !== undefined) q.search = params.search;
    if (params.subscriptionStatus !== undefined) {
      q.subscription_status = params.subscriptionStatus;
    }
    return q;
  }

  private async executeWithBearerAuth<T>(
    config: AxiosRequestConfig,
    retried = false,
  ): Promise<T> {
    const token = await this.getToken();
    try {
      const response: AxiosResponse<T> = await this.http.request<T>({
        ...config,
        headers: {
          Authorization: `Bearer ${token}`,
          ...config.headers,
        },
      });
      return response.data;
    } catch (err: unknown) {
      if (this.isUnauthorizedAxios(err) && !retried) {
        this.invalidateMasterToken();
        return this.executeWithBearerAuth<T>(config, true);
      }
      if (this.isUnauthorizedAxios(err) && retried) {
        throw new InternalServerErrorException(
          'Master API authentication failed after refresh',
        );
      }
      throw this.mapUpstreamError(err);
    }
  }

  private invalidateMasterToken(): void {
    this.tokenCache = null;
    this.inflightTokenPromise = null;
  }

  private async getToken(depth = 0): Promise<string> {
    const now = Date.now();
    if (depth > 4) {
      throw new InternalServerErrorException(
        'Unable to acquire a valid master API token',
      );
    }
    if (this.tokenCache && this.tokenCache.expiresAt > now) {
      return this.tokenCache.token;
    }

    if (!this.inflightTokenPromise) {
      this.inflightTokenPromise = this.issueToken().finally(() => {
        this.inflightTokenPromise = null;
      });
    }

    await this.inflightTokenPromise;
    const after = Date.now();
    if (this.tokenCache && this.tokenCache.expiresAt > after) {
      return this.tokenCache.token;
    }
    this.invalidateMasterToken();
    return this.getToken(depth + 1);
  }

  private async issueToken(): Promise<MasterTokenCache> {
    if (process.env.NODE_ENV === 'test') {
      const methods = this.configService.get<string[]>(
        'MASTER_API_ALLOWED_METHODS',
      ) ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
      const cache: MasterTokenCache = {
        token: MOCK_TEST_TOKEN,
        expiresAt: Date.now() + 365 * 24 * 3600 * 1000,
        allowedMethods: methods,
      };
      this.tokenCache = cache;
      return cache;
    }

    const secret = this.configService.get<string>('MASTER_API_SECRET');
    const methods =
      this.configService.get<string[]>('MASTER_API_ALLOWED_METHODS') ?? [];

    try {
      const response = await this.http.post<MasterTokenIssuanceResponse>(
        '/v1/master/token',
        { secret, methods },
      );

      const { access_token, expires_at, allowed_methods } = response.data;
      const parsed = expires_at ? Date.parse(expires_at) : NaN;
      let expiresAtMs: number;
      if (!Number.isNaN(parsed)) {
        expiresAtMs = parsed - TOKEN_REFRESH_MARGIN_MS;
      } else {
        const ttl =
          this.configService.get<number>('MASTER_API_TOKEN_TTL_SECONDS') ??
          3600;
        expiresAtMs = Date.now() + ttl * 1000 - TOKEN_REFRESH_MARGIN_MS;
      }

      const cache: MasterTokenCache = {
        token: access_token,
        expiresAt: expiresAtMs,
        allowedMethods: allowed_methods ?? methods,
      };
      this.tokenCache = cache;
      return cache;
    } catch (err: unknown) {
      const status = isAxiosError(err) ? err.response?.status : undefined;

      const message =
        isAxiosError(err) &&
        typeof err.response?.data === 'object' &&
        err.response?.data &&
        'message' in err.response.data
          ? String((err.response.data as { message: unknown }).message)
          : err instanceof Error
            ? err.message
            : 'Unknown error';

      this.logger.error(
        `Master token issuance failed: status=${status ?? 'n/a'}, message=${message}, url=/v1/master/token`,
      );

      if (status === 401) {
        throw new InternalServerErrorException(
          'Master API token issuance failed; check MASTER_API_SECRET',
        );
      }

      throw this.mapUpstreamError(err);
    }
  }

  private isUnauthorizedAxios(err: unknown): boolean {
    return isAxiosError(err) && err.response?.status === 401;
  }

  private mapUpstreamError(error: unknown): never {
    if (!isAxiosError(error) || error.response === undefined) {
      this.logger.error('Master API network or unknown error');
      throw new BadGatewayException('Master API upstream error');
    }

    const status = error.response.status;
    const rawData = error.response.data;
    const data =
      typeof rawData === 'object' && rawData !== null && !Array.isArray(rawData)
        ? (rawData as Record<string, unknown>)
        : {};
    const msg =
      typeof data.message === 'string'
        ? data.message
        : error.message || `HTTP ${status}`;

    const safeLog = this.maskSensitiveForLog(rawData);
    this.logger.warn(
      `Master API error: status=${status}, url=${error.config?.url}, data=${JSON.stringify(safeLog)}`,
    );

    switch (status) {
      case 403:
        throw new UnauthorizedException(msg);
      case 422: {
        const errorsObj = Object.prototype.hasOwnProperty.call(data, 'errors')
          ? data.errors
          : undefined;
        throw new BadRequestException({
          message: msg,
          ...(errorsObj !== undefined ? { errors: errorsObj } : {}),
        });
      }
      default:
        if (status >= 500) {
          throw new BadGatewayException('Master API upstream error');
        }
        throw new BadGatewayException(msg);
    }
  }

  private maskSensitiveForLog(data: unknown): unknown {
    if (!data || typeof data !== 'object' || Array.isArray(data)) {
      return data;
    }
    const copy = { ...(data as Record<string, unknown>) };
    if ('access_token' in copy) {
      copy.access_token = '[REDACTED]';
    }
    return copy;
  }
}

interface MasterTokenIssuanceResponse {
  access_token: string;
  expires_at: string;
  allowed_methods?: string[];
}

interface LaravelPaginatedRaw {
  data: LaravelMasterUserRow[];
  meta?: {
    current_page?: number;
    per_page?: number;
    total?: number;
    last_page?: number;
  };
}
