import { BadGatewayException, Injectable, ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PaymentProvider, Prisma } from '@prisma/client';
import { randomUUID } from 'crypto';
import { apiPrefix, apiVersion } from '../common/api-paths';
import { IntegrationConfigService } from '../integrations/integration-config.service';

type GatewayMode = 'sandbox' | 'live';

type InitializeGatewayPaymentInput = {
  provider: PaymentProvider;
  reference: string;
  amountKobo: number;
  currency: string;
  customerEmail: string;
  customerName: string;
  customerPhone: string;
  estateId: string;
  estateName: string;
  planCode: string;
  planName: string;
  metadata?: Record<string, unknown>;
};

type InitializedGatewayPayment = {
  provider: PaymentProvider;
  mode: GatewayMode;
  reference: string;
  providerReference: string | null;
  checkoutUrl: string;
  accessCode?: string | null;
  raw: unknown;
};

type VerifyGatewayPaymentInput = {
  provider: PaymentProvider;
  reference: string;
  amountKobo?: number | null;
  currency: string;
  metadata?: Prisma.JsonValue | null;
};

export type GatewayVerificationResult = {
  provider: PaymentProvider;
  mode: GatewayMode;
  reference: string;
  providerReference: string | null;
  paid: boolean;
  status: string;
  amountKobo: number | null;
  currency: string | null;
  raw: unknown;
  verifiedAt: string;
};

type ProviderRuntimeStatus = {
  provider: PaymentProvider;
  mode: GatewayMode;
  configured: boolean;
  baseUrl: string;
};

@Injectable()
export class PaymentGatewayService {
  constructor(
    private readonly config: ConfigService,
    private readonly integrations: IntegrationConfigService
  ) {}

  createReference(provider: PaymentProvider) {
    return `${provider.slice(0, 3)}-${Date.now()}-${randomUUID().slice(0, 8)}`;
  }

  getProviderRuntime(provider: PaymentProvider): ProviderRuntimeStatus {
    return {
      provider,
      mode: this.mode,
      configured: this.isProviderConfigured(provider),
      baseUrl: this.providerBaseUrl(provider)
    };
  }

  isProviderConfigured(provider: PaymentProvider) {
    switch (provider) {
      case PaymentProvider.FLUTTERWAVE:
        return this.hasUsableValue(this.configValue(provider, 'FLUTTERWAVE_SECRET_KEY'));
      case PaymentProvider.MONNIFY:
        return (
          this.hasUsableValue(this.configValue(provider, 'MONNIFY_API_KEY')) &&
          this.hasUsableValue(this.configValue(provider, 'MONNIFY_SECRET_KEY')) &&
          this.hasUsableValue(this.configValue(provider, 'MONNIFY_CONTRACT_CODE'))
        );
      case PaymentProvider.PAYSTACK:
        return this.hasUsableValue(this.configValue(provider, 'PAYSTACK_SECRET_KEY'));
      case PaymentProvider.STRIPE:
        return this.hasUsableValue(this.configValue(provider, 'STRIPE_SECRET_KEY'));
      default:
        return false;
    }
  }

  shouldVerifyGatewayPayment(metadata?: Prisma.JsonValue | null) {
    const gateway = this.gatewayMetadata(metadata);
    return gateway.initialized === true;
  }

  async initializePayment(input: InitializeGatewayPaymentInput): Promise<InitializedGatewayPayment> {
    switch (input.provider) {
      case PaymentProvider.FLUTTERWAVE:
        return this.initializeFlutterwavePayment(input);
      case PaymentProvider.MONNIFY:
        return this.initializeMonnifyPayment(input);
      case PaymentProvider.PAYSTACK:
        return this.initializePaystackPayment(input);
      case PaymentProvider.STRIPE:
        return this.initializeStripePayment(input);
      default:
        throw new ServiceUnavailableException('Payment provider is not supported');
    }
  }

  async verifyPayment(input: VerifyGatewayPaymentInput): Promise<GatewayVerificationResult> {
    switch (input.provider) {
      case PaymentProvider.FLUTTERWAVE:
        return this.verifyFlutterwavePayment(input);
      case PaymentProvider.MONNIFY:
        return this.verifyMonnifyPayment(input);
      case PaymentProvider.PAYSTACK:
        return this.verifyPaystackPayment(input);
      case PaymentProvider.STRIPE:
        return this.verifyStripePayment(input);
      default:
        throw new ServiceUnavailableException('Payment provider is not supported');
    }
  }

  private async initializePaystackPayment(input: InitializeGatewayPaymentInput): Promise<InitializedGatewayPayment> {
    const secretKey = this.requiredConfig('PAYSTACK_SECRET_KEY', input.provider);
    const payload = {
      email: input.customerEmail,
      amount: input.amountKobo,
      currency: input.currency,
      reference: input.reference,
      callback_url: this.callbackUrl(input.provider, input.reference),
      metadata: {
        estateId: input.estateId,
        planCode: input.planCode,
        customerPhone: input.customerPhone,
        ...(input.metadata ?? {})
      }
    };

    const raw = await this.fetchJson<{
      status: boolean;
      message?: string;
      data?: { authorization_url?: string; access_code?: string; reference?: string };
    }>(`${this.providerBaseUrl(input.provider)}/transaction/initialize`, input.provider, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${secretKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    const checkoutUrl = raw.data?.authorization_url;
    if (!raw.status || !checkoutUrl) {
      throw new BadGatewayException(raw.message ?? 'Paystack did not return an authorization URL');
    }

    return {
      provider: input.provider,
      mode: this.mode,
      reference: input.reference,
      providerReference: raw.data?.reference ?? input.reference,
      checkoutUrl,
      accessCode: raw.data?.access_code ?? null,
      raw
    };
  }

  private async initializeFlutterwavePayment(input: InitializeGatewayPaymentInput): Promise<InitializedGatewayPayment> {
    const secretKey = this.requiredConfig('FLUTTERWAVE_SECRET_KEY', input.provider);
    const payload = {
      tx_ref: input.reference,
      amount: this.toMajorCurrencyAmount(input.amountKobo),
      currency: input.currency,
      redirect_url: this.callbackUrl(input.provider, input.reference),
      customer: {
        email: input.customerEmail,
        phonenumber: input.customerPhone,
        name: input.customerName
      },
      customizations: {
        title: 'Smart Estate subscription',
        description: `${input.planName} subscription for ${input.estateName}`
      },
      meta: {
        estateId: input.estateId,
        planCode: input.planCode,
        ...(input.metadata ?? {})
      }
    };

    const raw = await this.fetchJson<{
      status: string;
      message?: string;
      data?: { link?: string };
    }>(`${this.providerBaseUrl(input.provider)}/payments`, input.provider, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${secretKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    const checkoutUrl = raw.data?.link;
    if (raw.status !== 'success' || !checkoutUrl) {
      throw new BadGatewayException(raw.message ?? 'Flutterwave did not return a checkout link');
    }

    return {
      provider: input.provider,
      mode: this.mode,
      reference: input.reference,
      providerReference: input.reference,
      checkoutUrl,
      raw
    };
  }

  private async initializeMonnifyPayment(input: InitializeGatewayPaymentInput): Promise<InitializedGatewayPayment> {
    const accessToken = await this.monnifyAccessToken(input.provider);
    const contractCode = this.requiredConfig('MONNIFY_CONTRACT_CODE', input.provider);
    const payload = {
      amount: this.toMajorCurrencyAmount(input.amountKobo),
      customerName: input.customerName,
      customerEmail: input.customerEmail,
      paymentReference: input.reference,
      paymentDescription: `${input.planName} subscription for ${input.estateName}`,
      currencyCode: input.currency,
      contractCode,
      redirectUrl: this.callbackUrl(input.provider, input.reference),
      paymentMethods: ['CARD', 'ACCOUNT_TRANSFER'],
      metaData: {
        estateId: input.estateId,
        planCode: input.planCode,
        customerPhone: input.customerPhone,
        ...(input.metadata ?? {})
      }
    };

    const raw = await this.fetchJson<{
      requestSuccessful?: boolean;
      responseMessage?: string;
      responseBody?: { checkoutUrl?: string; transactionReference?: string; paymentReference?: string };
    }>(`${this.providerBaseUrl(input.provider)}/api/v1/merchant/transactions/init-transaction`, input.provider, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(payload)
    });

    const checkoutUrl = raw.responseBody?.checkoutUrl;
    if (!raw.requestSuccessful || !checkoutUrl) {
      throw new BadGatewayException(raw.responseMessage ?? 'Monnify did not return a checkout URL');
    }

    return {
      provider: input.provider,
      mode: this.mode,
      reference: raw.responseBody?.paymentReference ?? input.reference,
      providerReference: raw.responseBody?.transactionReference ?? null,
      checkoutUrl,
      raw
    };
  }

  private async initializeStripePayment(input: InitializeGatewayPaymentInput): Promise<InitializedGatewayPayment> {
    const secretKey = this.requiredConfig('STRIPE_SECRET_KEY', input.provider);
    const body = new URLSearchParams();
    body.set('mode', 'payment');
    body.set('client_reference_id', input.reference);
    body.set('success_url', this.stripeReturnUrl(input.reference, true));
    body.set('cancel_url', this.stripeReturnUrl(input.reference, false));
    body.set('line_items[0][quantity]', '1');
    body.set('line_items[0][price_data][currency]', input.currency.toLowerCase());
    body.set('line_items[0][price_data][unit_amount]', String(input.amountKobo));
    body.set('line_items[0][price_data][product_data][name]', `${input.planName} subscription`);
    body.set('line_items[0][price_data][product_data][description]', `${input.estateName} estate subscription`);
    body.set('metadata[reference]', input.reference);
    body.set('metadata[estateId]', input.estateId);
    body.set('metadata[planCode]', input.planCode);
    body.set('payment_intent_data[metadata][reference]', input.reference);
    body.set('payment_intent_data[metadata][estateId]', input.estateId);
    body.set('payment_intent_data[metadata][planCode]', input.planCode);

    const raw = await this.fetchJson<{ id?: string; url?: string; payment_intent?: string | null }>(
      `${this.providerBaseUrl(input.provider)}/v1/checkout/sessions`,
      input.provider,
      {
        method: 'POST',
        headers: {
          Authorization: `Basic ${Buffer.from(`${secretKey}:`).toString('base64')}`,
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body
      }
    );

    if (!raw.id || !raw.url) {
      throw new BadGatewayException('Stripe did not return a checkout session URL');
    }

    return {
      provider: input.provider,
      mode: this.mode,
      reference: input.reference,
      providerReference: raw.id,
      checkoutUrl: raw.url,
      raw
    };
  }

  private async verifyPaystackPayment(input: VerifyGatewayPaymentInput): Promise<GatewayVerificationResult> {
    const secretKey = this.requiredConfig('PAYSTACK_SECRET_KEY', input.provider);
    const raw = await this.fetchJson<{
      status: boolean;
      message?: string;
      data?: { status?: string; reference?: string; amount?: number; currency?: string };
    }>(`${this.providerBaseUrl(input.provider)}/transaction/verify/${encodeURIComponent(input.reference)}`, input.provider, {
      headers: { Authorization: `Bearer ${secretKey}` }
    });
    const data = raw.data;

    return this.verificationResult(input, {
      providerReference: data?.reference ?? input.reference,
      status: data?.status ?? raw.message ?? 'unknown',
      amountKobo: data?.amount ?? null,
      currency: data?.currency ?? null,
      raw,
      paid: raw.status === true && data?.status === 'success'
    });
  }

  private async verifyFlutterwavePayment(input: VerifyGatewayPaymentInput): Promise<GatewayVerificationResult> {
    const secretKey = this.requiredConfig('FLUTTERWAVE_SECRET_KEY', input.provider);
    const gateway = this.gatewayMetadata(input.metadata);
    const transactionId = this.stringValue(gateway.providerTransactionId);
    const endpoint = transactionId
      ? `${this.providerBaseUrl(input.provider)}/transactions/${encodeURIComponent(transactionId)}/verify`
      : `${this.providerBaseUrl(input.provider)}/transactions/verify_by_reference?tx_ref=${encodeURIComponent(input.reference)}`;

    const raw = await this.fetchJson<{
      status: string;
      message?: string;
      data?: { id?: number; status?: string; tx_ref?: string; amount?: number; currency?: string };
    }>(endpoint, input.provider, {
      headers: { Authorization: `Bearer ${secretKey}` }
    });
    const data = raw.data;

    return this.verificationResult(input, {
      providerReference: data?.id ? String(data.id) : (data?.tx_ref ?? input.reference),
      status: data?.status ?? raw.message ?? 'unknown',
      amountKobo: data?.amount === undefined ? null : this.toKobo(data.amount),
      currency: data?.currency ?? null,
      raw,
      paid: raw.status === 'success' && data?.status === 'successful'
    });
  }

  private async verifyMonnifyPayment(input: VerifyGatewayPaymentInput): Promise<GatewayVerificationResult> {
    const accessToken = await this.monnifyAccessToken(input.provider);
    const gateway = this.gatewayMetadata(input.metadata);
    const transactionReference = this.stringValue(gateway.providerReference);
    const query = transactionReference
      ? `transactionReference=${encodeURIComponent(transactionReference)}`
      : `paymentReference=${encodeURIComponent(input.reference)}`;

    const raw = await this.fetchJson<{
      requestSuccessful?: boolean;
      responseMessage?: string;
      responseBody?: {
        transactionReference?: string;
        paymentReference?: string;
        paymentStatus?: string;
        amountPaid?: number;
        amount?: number;
        currencyCode?: string;
      };
    }>(`${this.providerBaseUrl(input.provider)}/api/v2/merchant/transactions/query?${query}`, input.provider, {
      headers: { Authorization: `Bearer ${accessToken}` }
    });
    const data = raw.responseBody;
    const status = data?.paymentStatus ?? raw.responseMessage ?? 'unknown';

    return this.verificationResult(input, {
      providerReference: data?.transactionReference ?? transactionReference ?? input.reference,
      status,
      amountKobo: data?.amountPaid === undefined && data?.amount === undefined ? null : this.toKobo(data.amountPaid ?? data.amount ?? 0),
      currency: data?.currencyCode ?? null,
      raw,
      paid: raw.requestSuccessful === true && ['PAID', 'SUCCESS', 'SUCCESSFUL'].includes(status.toUpperCase())
    });
  }

  private async verifyStripePayment(input: VerifyGatewayPaymentInput): Promise<GatewayVerificationResult> {
    const secretKey = this.requiredConfig('STRIPE_SECRET_KEY', input.provider);
    const gateway = this.gatewayMetadata(input.metadata);
    const sessionId = this.stringValue(gateway.providerReference) ?? this.stringValue(gateway.providerTransactionId);
    if (!sessionId) {
      throw new BadGatewayException('Stripe checkout session id is missing from payment metadata');
    }

    const raw = await this.fetchJson<{
      id?: string;
      status?: string;
      payment_status?: string;
      amount_total?: number | null;
      currency?: string | null;
    }>(`${this.providerBaseUrl(input.provider)}/v1/checkout/sessions/${encodeURIComponent(sessionId)}`, input.provider, {
      headers: { Authorization: `Basic ${Buffer.from(`${secretKey}:`).toString('base64')}` }
    });

    return this.verificationResult(input, {
      providerReference: raw.id ?? sessionId,
      status: raw.payment_status ?? raw.status ?? 'unknown',
      amountKobo: raw.amount_total ?? null,
      currency: raw.currency?.toUpperCase() ?? null,
      raw,
      paid: raw.payment_status === 'paid'
    });
  }

  private verificationResult(
    input: VerifyGatewayPaymentInput,
    data: {
      providerReference: string | null;
      paid: boolean;
      status: string;
      amountKobo: number | null;
      currency: string | null;
      raw: unknown;
    }
  ): GatewayVerificationResult {
    const amountMatches = input.amountKobo === null || input.amountKobo === undefined || data.amountKobo === null || data.amountKobo === input.amountKobo;
    const currencyMatches =
      !input.currency || data.currency === null || data.currency.toUpperCase() === input.currency.toUpperCase();

    return {
      provider: input.provider,
      mode: this.mode,
      reference: input.reference,
      providerReference: data.providerReference,
      paid: data.paid && amountMatches && currencyMatches,
      status: data.status,
      amountKobo: data.amountKobo,
      currency: data.currency,
      raw: data.raw,
      verifiedAt: new Date().toISOString()
    };
  }

  private async monnifyAccessToken(provider: PaymentProvider) {
    const apiKey = this.requiredConfig('MONNIFY_API_KEY', provider);
    const secretKey = this.requiredConfig('MONNIFY_SECRET_KEY', provider);
    const raw = await this.fetchJson<{
      requestSuccessful?: boolean;
      responseMessage?: string;
      responseBody?: { accessToken?: string };
    }>(`${this.providerBaseUrl(provider)}/api/v1/auth/login`, provider, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString('base64')}`
      }
    });

    const token = raw.responseBody?.accessToken;
    if (!raw.requestSuccessful || !token) {
      throw new BadGatewayException(raw.responseMessage ?? 'Monnify did not return an access token');
    }
    return token;
  }

  private async fetchJson<T>(url: string, provider: PaymentProvider, init: RequestInit = {}): Promise<T> {
    let response: Response;
    try {
      response = await fetch(url, init);
    } catch (error) {
      throw new BadGatewayException(`${provider} sandbox request failed: ${String(error)}`);
    }

    const text = await response.text();
    const payload = text ? this.parseJson(text) : {};
    if (!response.ok) {
      throw new BadGatewayException(`${provider} sandbox request failed: ${this.gatewayMessage(payload)}`);
    }
    return payload as T;
  }

  private parseJson(text: string): unknown {
    try {
      return JSON.parse(text);
    } catch {
      return { message: text };
    }
  }

  private gatewayMessage(payload: unknown) {
    if (payload && typeof payload === 'object') {
      const data = payload as Record<string, unknown>;
      return (
        this.stringValue(data.message) ??
        this.stringValue(data.responseMessage) ??
        this.stringValue(data.error) ??
        'Unexpected gateway response'
      );
    }
    return 'Unexpected gateway response';
  }

  private requiredConfig(key: string, provider: PaymentProvider) {
    const value = this.configValue(provider, key);
    if (!this.hasUsableValue(value)) {
      throw new ServiceUnavailableException(`${provider} credentials are not configured. Set ${key} in .env or update /admin/integrations/config/${provider}.`);
    }
    return value as string;
  }

  private hasUsableValue(value?: string | null) {
    return Boolean(value && !/REPLACE_ME|YOUR_|your-|placeholder/i.test(value));
  }

  private providerBaseUrl(provider: PaymentProvider) {
    switch (provider) {
      case PaymentProvider.FLUTTERWAVE:
        return this.configValue(provider, 'FLUTTERWAVE_BASE_URL', 'https://api.flutterwave.com/v3');
      case PaymentProvider.MONNIFY:
        return this.configValue(provider, 'MONNIFY_BASE_URL', this.mode === 'live' ? 'https://api.monnify.com' : 'https://sandbox.monnify.com');
      case PaymentProvider.PAYSTACK:
        return this.configValue(provider, 'PAYSTACK_BASE_URL', 'https://api.paystack.co');
      case PaymentProvider.STRIPE:
        return this.configValue(provider, 'STRIPE_BASE_URL', 'https://api.stripe.com');
      default:
        return '';
    }
  }

  private callbackUrl(provider: PaymentProvider, reference: string) {
    const configured = this.config.get<string>('PAYMENT_CALLBACK_URL');
    const base = configured ?? `${this.apiBaseUrl()}/v${apiVersion(this.config)}/onboarding/payments/callback`;
    const separator = base.includes('?') ? '&' : '?';
    return `${base}${separator}provider=${provider}&reference=${encodeURIComponent(reference)}`;
  }

  private stripeReturnUrl(reference: string, success: boolean) {
    const configured = this.configValue(PaymentProvider.STRIPE, success ? 'STRIPE_SUCCESS_URL' : 'STRIPE_CANCEL_URL');
    if (configured) {
      return configured
        .replaceAll('{reference}', encodeURIComponent(reference))
        .replaceAll('{provider}', PaymentProvider.STRIPE)
        .replaceAll('{CHECKOUT_SESSION_ID}', '{CHECKOUT_SESSION_ID}');
    }

    const outcome = success ? 'success' : 'cancelled';
    return `${this.apiBaseUrl()}/v${apiVersion(this.config)}/onboarding/payments/callback?provider=STRIPE&reference=${encodeURIComponent(
      reference
    )}&status=${outcome}&session_id={CHECKOUT_SESSION_ID}`;
  }

  private apiBaseUrl() {
    const configured = this.config.get<string>('PUBLIC_API_BASE_URL');
    if (configured) return configured.replace(/\/$/, '');
    const port = this.config.get<string>('API_PORT') ?? '4000';
    return `http://localhost:${port}/${apiPrefix(this.config)}`;
  }

  private toMajorCurrencyAmount(amountKobo: number) {
    return Math.round(amountKobo) / 100;
  }

  private toKobo(amount: number) {
    return Math.round(amount * 100);
  }

  private gatewayMetadata(metadata?: Prisma.JsonValue | null): Record<string, unknown> {
    if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
      return {};
    }
    const root = metadata as Record<string, unknown>;
    const gateway = root.gateway;
    if (!gateway || typeof gateway !== 'object' || Array.isArray(gateway)) {
      return {};
    }
    return gateway as Record<string, unknown>;
  }

  private stringValue(value: unknown) {
    return typeof value === 'string' && value.length > 0 ? value : null;
  }

  private get mode(): GatewayMode {
    return this.config.get<string>('PAYMENT_GATEWAY_MODE') === 'live' ? 'live' : 'sandbox';
  }

  private configValue(provider: PaymentProvider, envKey: string, fallback = '') {
    return this.integrations.getValue(provider, this.integrationKey(envKey), envKey, fallback);
  }

  private integrationKey(envKey: string) {
    const key = envKey
      .replace(/^(FLUTTERWAVE|MONNIFY|PAYSTACK|STRIPE)_/, '')
      .toLowerCase()
      .replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
    if (key === 'baseUrl') return 'baseUrl';
    if (key === 'publicKey') return 'publicKey';
    if (key === 'secretKey') return 'secretKey';
    if (key === 'encryptionKey') return 'encryptionKey';
    if (key === 'apiKey') return 'apiKey';
    if (key === 'contractCode') return 'contractCode';
    if (key === 'successUrl') return 'successUrl';
    if (key === 'cancelUrl') return 'cancelUrl';
    return key;
  }
}
