import {
  BadRequestException,
  Injectable,
  Logger,
  ServiceUnavailableException,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import axios, { AxiosInstance } from 'axios';
import * as bcrypt from 'bcrypt';
import { randomBytes } from 'crypto';
import { PrismaService } from '../../common/prisma.service';
import { FaydaConfig, FaydaPlatform } from '../../config/fayda.config';
import {
  generateCodeChallenge,
  generateCodeVerifier,
  generateState,
} from './utils/pkce.util';
import { generateClientAssertion } from './utils/client-assertion.util';
import { VerifaydaCallbackDto, VerificationStatusDto } from './verifayda.dto';
import {
  FaydaIdentityConflictException,
  FaydaTokenExchangeException,
  FaydaUserInfoException,
} from './verifayda.errors';
import {
  FaydaTokenResponse,
  FaydaUserInfo,
  NormalizedFaydaUserInfo,
  VerifaydaPurpose,
} from './verifayda.types';

export interface VerifaydaPassengerData {
  fullName: string;
  dateOfBirth: Date;
  gender?: string;
  nationality?: string;
  profileData?: Record<string, any>;
}

export interface VerifaydaVerificationResult {
  verified: boolean;
  passengerData?: VerifaydaPassengerData;
  failureReason?: string;
}

export interface StartVerificationInput {
  purpose: VerifaydaPurpose;
  platform?: FaydaPlatform;
  userId?: string;
  bookingId?: string;
  saveToAccount?: boolean;
}

export interface FaydaUserSummary {
  id: string;
  email: string;
  role: string;
  passengerId?: string;
  agentId?: string;
}

/**
 * Result of completing a verification. `verified` is always true on success.
 * LOGIN additionally returns a JWT + user; PURCHASE returns the verified name.
 */
export interface CompleteVerificationResult {
  purpose: VerifaydaPurpose;
  verified: boolean;
  token?: string;
  user?: FaydaUserSummary;
  fullName?: string;
}

@Injectable()
export class VerifaydaService {
  private readonly logger = new Logger(VerifaydaService.name);


  private readonly faydaConfig: FaydaConfig;

  private readonly httpClient: AxiosInstance;
  private readonly stubEnabled: boolean;
  private readonly stubApiUrl: string;
  private readonly stubApiKey: string;

  constructor(
    private readonly config: ConfigService,
    private readonly prisma: PrismaService,
    private readonly jwt: JwtService,
  ) {
    const fayda = this.config.get<FaydaConfig>('fayda');
    if (!fayda) {
      throw new Error('Fayda config namespace not registered');
    }
    this.faydaConfig = fayda;

    this.stubEnabled = this.config.get<boolean>('VERIFAYDA_ENABLED', false);
    this.stubApiUrl = this.config.get<string>(
      'VERIFAYDA_API_URL',
      'https://api.verifayda.gov.et/v2',
    );
    this.stubApiKey = this.config.get<string>('VERIFAYDA_API_KEY', '');
    
    this.logger.log(`Verifayda configuration: enabled=${this.stubEnabled}, url=${this.stubApiUrl}`);
    
    // Only create HTTP client if Verifayda is enabled
    if (this.stubEnabled) {
      this.httpClient = axios.create({
        baseURL: this.stubApiUrl,
        timeout: 10000,
        headers: { 'Content-Type': 'application/json', 'X-API-Key': this.stubApiKey },
      });
      this.logger.log('Verifayda HTTP client created');
    } else {
      this.logger.log('Verifayda HTTP client NOT created (disabled)');
    }
  }

  // ==========================================================================
  // OIDC flow
  // ==========================================================================

  async startVerification(input: StartVerificationInput): Promise<string> {
    if (!this.faydaConfig.enabled) {
      throw new ServiceUnavailableException({
        code: 'FAYDA_DISABLED',
        message: 'Fayda integration is not enabled',
      });
    }

    const state = generateState();
    const codeVerifier = generateCodeVerifier();
    const codeChallenge = generateCodeChallenge(codeVerifier);
    const expiresAt = new Date(
      Date.now() + this.faydaConfig.sessionTtlMinutes * 60_000,
    );

    await this.prisma.faydaVerificationSession.create({
      data: {
        state,
        codeVerifier,
        purpose: input.purpose,
        platform: input.platform ?? 'WEB',
        saveToAccount: input.saveToAccount ?? false,
        userId: input.userId ?? null,
        bookingId: input.bookingId ?? null,
        expiresAt,
      },
    });

    this.logger.log(
      `Fayda verification started: purpose=${input.purpose} platform=${input.platform ?? 'WEB'} userId=${input.userId ?? 'none'} bookingId=${input.bookingId ?? 'none'}`,
    );

    return this.buildAuthorizationUrl({ state, codeChallenge });
  }


  async completeVerification(
    query: VerifaydaCallbackDto,
  ): Promise<CompleteVerificationResult> {
    if (query.error) {
      this.logger.warn(`Fayda callback returned error: ${query.error}`);
      if (query.state) {
        await this.markSessionFailed(
          query.state,
          query.error,
          query.error_description,
        );
      }
      throw new BadRequestException({
        code: 'FAYDA_AUTH_ERROR',
        message: query.error,
        description: query.error_description,
      });
    }

    if (!query.code || !query.state) {
      throw new BadRequestException({
        code: 'FAYDA_MISSING_PARAMETERS',
        message: 'code and state are required',
      });
    }

    const session = await this.prisma.faydaVerificationSession.findUnique({
      where: { state: query.state },
    });
    if (!session || session.status !== 'PENDING') {
      this.logger.warn('Fayda complete with unknown or non-pending state');
      throw new BadRequestException({
        code: 'FAYDA_INVALID_STATE',
        message: 'Verification session is invalid or already used',
      });
    }
    if (session.expiresAt.getTime() < Date.now()) {
      await this.markSessionFailed(query.state, 'session_expired');
      throw new BadRequestException({
        code: 'FAYDA_SESSION_EXPIRED',
        message: 'Verification session has expired; start again',
      });
    }

    try {
      const tokens = await this.exchangeCodeForTokens(
        query.code,
        session.codeVerifier,
      );
      const userInfo = await this.fetchUserInfo(tokens.access_token);
      const normalized = this.normalizeUserInfo(userInfo);

      if (!normalized.sub) {
        throw new FaydaUserInfoException('Fayda userinfo missing required sub');
      }

      let result: CompleteVerificationResult;
      if (session.purpose === 'PURCHASE') {
        await this.handlePurchaseSuccess(session, normalized);
        result = {
          purpose: 'PURCHASE',
          verified: true,
          fullName: normalized.fullName,
        };
      } else {
        const { userId } = await this.handleLoginSuccess(normalized);
        const login = await this.issueLoginToken(userId);
        result = { purpose: 'LOGIN', verified: true, ...login };
      }

      await this.prisma.faydaVerificationSession.update({
        where: { id: session.id },
        data: { status: 'COMPLETED', completedAt: new Date(), codeVerifier: '' },
      });

      this.logger.log(
        `Fayda verification completed: purpose=${session.purpose} platform=${session.platform}`,
      );
      return result;
    } catch (err) {
      const reason = this.classifyFailureReason(err);
      this.logger.error(
        `Fayda verification failed: reason=${reason} message=${(err as Error).message}`,
      );
      await this.markSessionFailed(
        query.state,
        reason,
        (err as Error).message,
      );
      throw err;
    }
  }

  /** Loads a user (+ relations) and mints the same JWT shape as `/auth/login`. */
  private async issueLoginToken(
    userId: string,
  ): Promise<{ token: string; user: FaydaUserSummary }> {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      include: { passenger: true, agent: true },
    });
    if (!user) {
      // Should not happen — we just resolved/created this user.
      throw new UnauthorizedException({
        code: 'FAYDA_LOGIN_FAILED',
        message: 'Could not load the verified user',
      });
    }

    const summary: FaydaUserSummary = {
      id: user.id,
      email: user.email,
      role: user.role,
      passengerId: user.passenger?.id,
      agentId: user.agent?.id,
    };
    const token = this.jwt.sign({
      sub: summary.id,
      email: summary.email,
      role: summary.role,
      passengerId: summary.passengerId,
      agentId: summary.agentId,
    });

    this.logger.log(`Fayda login issued token for user ${user.id}`);
    return { token, user: summary };
  }

  async getVerificationStatus(userId: string): Promise<VerificationStatusDto> {
    const user = await this.prisma.user.findUnique({
      where: { id: userId },
      select: { faydaVerified: true, faydaVerifiedAt: true, fullName: true },
    });

    return {
      verified: user?.faydaVerified ?? false,
      verifiedAt: user?.faydaVerifiedAt ?? undefined,
      fullName: user?.fullName ?? undefined,
    };
  }

  // ==========================================================================
  // OIDC internals
  // ==========================================================================

  private buildAuthorizationUrl(args: {
    state: string;
    codeChallenge: string;
  }): string {
    const params = new URLSearchParams({
      client_id: this.faydaConfig.clientId,
      response_type: 'code',
      redirect_uri: this.faydaConfig.redirectUri,
      scope: this.faydaConfig.scope,
      state: args.state,
      code_challenge: args.codeChallenge,
      code_challenge_method: 'S256',
      acr_values: this.faydaConfig.acrValues,
      claims_locales: this.faydaConfig.claimsLocales,
    });

    const claims = {
      userinfo: {
        name: { essential: true },
        phone_number: { essential: true },
        email: { essential: false },
        birthdate: { essential: true },
        gender: { essential: false },
        picture: { essential: false },
      },
      id_token: {},
    };
    params.set('claims', JSON.stringify(claims));

    return `${this.faydaConfig.authorizationEndpoint}?${params.toString()}`;
  }

  private async exchangeCodeForTokens(
    code: string,
    codeVerifier: string,
  ): Promise<FaydaTokenResponse> {
    const clientAssertion = await generateClientAssertion({
      clientId: this.faydaConfig.clientId,
      audience: this.faydaConfig.tokenEndpoint,
      privateJwk: this.faydaConfig.privateJwk,
    });

    const body = new URLSearchParams({
      grant_type: 'authorization_code',
      code,
      redirect_uri: this.faydaConfig.redirectUri,
      client_id: this.faydaConfig.clientId,
      client_assertion_type:
        'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
      client_assertion: clientAssertion,
      code_verifier: codeVerifier,
    });

    const response = await fetch(this.faydaConfig.tokenEndpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body,
    });

    if (!response.ok) {
      let detail = '';
      try {
        detail = await response.text();
      } catch {
        // ignore
      }
      throw new FaydaTokenExchangeException(
        `Fayda token endpoint returned ${response.status}${detail ? `: ${detail}` : ''}`,
      );
    }

    return (await response.json()) as FaydaTokenResponse;
  }

  private async fetchUserInfo(accessToken: string): Promise<FaydaUserInfo> {
    const response = await fetch(this.faydaConfig.userInfoEndpoint, {
      method: 'GET',
      headers: { Authorization: `Bearer ${accessToken}` },
    });

    if (!response.ok) {
      throw new FaydaUserInfoException(
        `Fayda userinfo endpoint returned ${response.status}`,
      );
    }

    const contentType = response.headers.get('content-type') ?? '';
    const raw = await response.text();

    if (contentType.includes('application/json')) {
      return JSON.parse(raw) as FaydaUserInfo;
    }

    // Signed JWT response — decode payload (signature verification = production TODO)
    if (raw.split('.').length === 3) {
      const payloadB64 = raw.split('.')[1];
      const normalizedB64 = payloadB64.replace(/-/g, '+').replace(/_/g, '/');
      const json = Buffer.from(normalizedB64, 'base64').toString('utf8');
      return JSON.parse(json) as FaydaUserInfo;
    }

    throw new FaydaUserInfoException(
      'Unsupported Fayda userinfo response format',
    );
  }

  private normalizeUserInfo(raw: FaydaUserInfo): NormalizedFaydaUserInfo {
    return {
      sub: raw.sub,
      fullName: raw.name ?? raw['name#en'] ?? raw['name#am'],
      phoneNumber:
        raw.phone_number ?? raw['phone_number#en'] ?? raw['phone_number#am'] ?? raw.phone,
      email: raw.email,
      gender: raw.gender,
      birthdate: raw.birthdate,
      picture: raw.picture,
    };
  }

  private async handlePurchaseSuccess(
    session: {
      id: string;
      userId: string | null;
      bookingId: string | null;
      saveToAccount: boolean;
    },
    normalized: NormalizedFaydaUserInfo,
  ): Promise<void> {
    if (session.bookingId) {
      await this.prisma.bookingSeat.updateMany({
        where: { bookingId: session.bookingId },
        data: {
          faydaVerifiedAt: new Date(),
          faydaSub: normalized.sub,
          faydaVerifiedName: normalized.fullName ?? null,
        },
      });
    }

    if (session.userId && session.saveToAccount) {
      const conflict = await this.prisma.user.findFirst({
        where: {
          faydaSub: normalized.sub,
          NOT: { id: session.userId },
        },
        select: { id: true },
      });
      if (conflict) {
        throw new FaydaIdentityConflictException();
      }

      await this.prisma.user.update({
        where: { id: session.userId },
        data: {
          faydaVerified: true,
          faydaVerifiedAt: new Date(),
          faydaSub: normalized.sub,
        },
      });
    }
  }

  /**
   * Resolves the User for a LOGIN flow and returns its id (the caller mints the
   * JWT via {@link issueLoginToken}). Resolution order:
   *   1. Existing user already linked to this Fayda `sub`.
   *   2. Existing account whose email/phone matches — linked to this `sub`.
   *   3. Otherwise a fresh Fayda-backed account is created.
   */
  private async handleLoginSuccess(
    normalized: NormalizedFaydaUserInfo,
  ): Promise<{ userId: string }> {
    let userId: string;

    const bySub = await this.prisma.user.findUnique({
      where: { faydaSub: normalized.sub },
      select: { id: true },
    });

    if (bySub) {
      userId = bySub.id;
    } else {
      const matchers: Array<{ email?: string; phone?: string }> = [];
      if (normalized.email) matchers.push({ email: normalized.email });
      if (normalized.phoneNumber) matchers.push({ phone: normalized.phoneNumber });

      const existing = matchers.length
        ? await this.prisma.user.findFirst({
            where: { OR: matchers },
            select: { id: true, faydaSub: true },
          })
        : null;

      if (existing) {
        if (existing.faydaSub && existing.faydaSub !== normalized.sub) {
          // The matched account is already tied to a different Fayda identity.
          throw new FaydaIdentityConflictException();
        }
        await this.prisma.user.update({
          where: { id: existing.id },
          data: {
            faydaSub: normalized.sub,
            faydaVerified: true,
            faydaVerifiedAt: new Date(),
          },
        });
        userId = existing.id;
        this.logger.log(`Fayda login linked existing user ${existing.id}`);
      } else {
        userId = await this.createFaydaUser(normalized);
        this.logger.log(`Fayda login created new user ${userId}`);
      }
    }

    return { userId };
  }

  /**
   * Creates a Fayda-backed User plus the same satellite rows registration makes
   * (Passenger, LoyaltyAccount, WalletAccount, UserPreferences).
   *
   * The user has no password — `passwordHash` is set to a bcrypt of random bytes
   * so password login is impossible; they authenticate only via Fayda. When
   * Fayda doesn't supply an email/phone, a deterministic placeholder derived from
   * the (unique) `sub` keeps the NOT NULL + unique columns satisfied.
   */
  private async createFaydaUser(
    normalized: NormalizedFaydaUserInfo,
  ): Promise<string> {
    const passwordHash = await bcrypt.hash(
      randomBytes(32).toString('hex'),
      10,
    );
    const email = normalized.email ?? `fayda_${normalized.sub}@users.fayda.local`;
    const phone = normalized.phoneNumber ?? `fayda:${normalized.sub}`;
    const fullName = normalized.fullName ?? 'Fayda User';

    const user = await this.prisma.user.create({
      data: {
        fullName,
        email,
        phone,
        passwordHash,
        faydaVerified: true,
        faydaVerifiedAt: new Date(),
        faydaSub: normalized.sub,
      },
      select: { id: true },
    });
    const passenger = await this.prisma.passenger.create({
      data: { userId: user.id },
      select: { id: true },
    });
    await this.prisma.loyaltyAccount.create({
      data: { passengerId: passenger.id },
    });
    await this.prisma.walletAccount.create({
      data: { passengerId: passenger.id },
    });
    await this.prisma.userPreferences.create({ data: { userId: user.id } });

    return user.id;
  }

  private async markSessionFailed(
    state: string,
    errorCode: string,
    errorDescription?: string,
  ): Promise<void> {
    await this.prisma.faydaVerificationSession.updateMany({
      where: { state, status: 'PENDING' },
      data: {
        status: 'FAILED',
        errorCode,
        errorDescription: errorDescription ?? null,
        completedAt: new Date(),
        codeVerifier: '',
      },
    });
  }

  private classifyFailureReason(err: unknown): string {
    if (err instanceof FaydaIdentityConflictException) return 'identity_conflict';
    if (err instanceof FaydaTokenExchangeException) return 'token_exchange_failed';
    if (err instanceof FaydaUserInfoException) return 'userinfo_failed';
    return 'verification_failed';
  }

  // ==========================================================================
  // DEPRECATED: legacy stub flow
  // ==========================================================================

  /** @deprecated Use the OIDC flow instead. Retained until cleanup. */
  async verifyNationalId(
    nationalId: string,
    bookingId?: string,
  ): Promise<VerifaydaVerificationResult> {
    this.logger.log(`verifyNationalId called: stubEnabled=${this.stubEnabled}, type=${typeof this.stubEnabled}`);
    
    if (this.stubEnabled != false || this.stubEnabled) {
      this.logger.warn('Verifayda stub is disabled - returning mock data (development mode)');
      // In development mode, return mock verified data
      return {
        verified: true,
        passengerData: {
          fullName: 'Mock Passenger',
          dateOfBirth: new Date('1990-01-01'),
          gender: 'Male',
          nationality: 'Ethiopian',
        },
      };
    }

    const requestPayload = {
      nationalId,
      requestedFields: ['fullName', 'dateOfBirth', 'gender', 'nationality'],
      timestamp: new Date().toISOString(),
    };

    try {
      this.logger.log('Verifying national ID via legacy Verifayda stub');
      const response = await this.httpClient.post('/verify', requestPayload);
      const { data } = response;

      if (data.status === 'verified' && data.citizen) {
        const passengerData: VerifaydaPassengerData = {
          fullName: data.citizen.fullName,
          dateOfBirth: new Date(data.citizen.dateOfBirth),
          gender: data.citizen.gender,
          nationality: data.citizen.nationality || 'Ethiopian',
          profileData: data.citizen,
        };

        await this.prisma.verifaydaVerification.create({
          data: {
            bookingId,
            nationalId,
            requestPayload,
            responsePayload: data,
            verified: true,
            verifiedAt: new Date(),
          },
        });

        return { verified: true, passengerData };
      }

      const failureReason = data.message || 'Verification failed';
      await this.prisma.verifaydaVerification.create({
        data: {
          bookingId,
          nationalId,
          requestPayload,
          responsePayload: data,
          verified: false,
          failureReason,
        },
      });
      return { verified: false, failureReason };
    } catch (error: any) {
      const errorMessage =
        error.response?.data?.message || error.message || 'Unknown error';
      await this.prisma.verifaydaVerification.create({
        data: {
          bookingId,
          nationalId,
          requestPayload,
          verified: false,
          failureReason: errorMessage,
        },
      });
      this.logger.error(`Verifayda stub error: ${errorMessage}`);
      throw new BadRequestException(
        `National ID verification failed: ${errorMessage}`,
      );
    }
  }

  /** @deprecated Use `faydaConfig.enabled` for the OIDC flow. */
  isEnabled(): boolean {
    return this.stubEnabled;
  }
}
