import { BadRequestException, ConflictException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import {
  AccountStatus,
  EstateSignupStatus,
  EstateStatus,
  PaymentProvider,
  PaymentStatus,
  Prisma,
  SubscriptionStatus,
  UserRole
} from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../database/prisma.service';
import { PaymentGatewayService } from '../payments/payment-gateway.service';
import { EstateAdminLeadSignupDto, InitializeEstatePaymentDto, RecordEstatePaymentDto, SelectSubscriptionPlanDto } from './dto/onboarding.dto';

@Injectable()
export class OnboardingService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
    private readonly paymentGateway: PaymentGatewayService
  ) {}

  async listSubscriptionPlans() {
    const plans = await this.prisma.subscriptionPlan.findMany({
      where: { isActive: true, publicVisible: true },
      orderBy: [{ sortOrder: 'asc' }, { minResidents: 'asc' }],
      include: { features: { orderBy: { feature: 'asc' } } }
    });
    return plans.map((plan) => {
      const { features, ...rest } = plan;
      return { ...rest, features: features.map((item) => item.feature) };
    });
  }

  async listPaymentProviders(estateId?: string) {
    const globals = await this.prisma.globalPaymentProviderSetting.findMany({ orderBy: { displayName: 'asc' } });
    const overrides = estateId
      ? await this.prisma.estatePaymentProviderSetting.findMany({ where: { estateId } })
      : [];

    return globals.map((global) => {
      const override = overrides.find((item) => item.provider === global.provider);
      return {
        provider: global.provider,
        displayName: global.displayName,
        globallyEnabled: global.enabled,
        estateEnabled: override?.enabled ?? null,
        effectiveEnabled: override?.enabled ?? global.enabled,
        recommendedUseCases: global.recommendedUseCases,
        metadata: global.metadata
      };
    });
  }

  async submitEstateAdminLead(dto: EstateAdminLeadSignupDto) {
    try {
      const result = await this.prisma.$transaction(async (tx) => {
        const estate = await tx.estate.create({
          data: {
            name: dto.estateName,
            location: dto.location,
            unitCount: dto.unitCount,
            estateType: dto.estateType,
            status: EstateStatus.PENDING,
            signupStatus: EstateSignupStatus.PENDING_APPROVAL,
            subscriptionStatus: SubscriptionStatus.NONE,
            settings: {
              onboarding: {
                adminEmail: dto.adminEmail ?? null,
                metadata: {
                  ...(dto.metadata ?? {}),
                  demoAccess: {
                    requested: dto.metadata?.source === 'public-demo-request',
                    estateLocation: dto.estateLocation ?? dto.location,
                    gateCount: dto.gateCount ?? null,
                    securityPersonnelCount: dto.securityPersonnelCount ?? null,
                    currentVisitorEntryProcess: dto.currentVisitorEntryProcess ?? null,
                    externalSecurityCompany: dto.externalSecurityCompany ?? null,
                    managementChallenges: dto.managementChallenges ?? null,
                    gateWiseBudget: dto.gateWiseBudget ?? null,
                    emailVerificationProvided: Boolean(dto.emailVerificationToken),
                    phoneVerificationProvided: Boolean(dto.phoneVerificationToken),
                    approvalRequired: true,
                    isolatedDemoDataOnly: true,
                    exportDisabled: true,
                    productionApiAccess: false
                  }
                }
              }
            } as Prisma.InputJsonValue
          }
        });

        const admin = await tx.user.create({
          data: {
            estateId: estate.id,
            phone: dto.adminPhone,
            name: dto.adminName,
            role: UserRole.ESTATE_ADMIN,
            status: AccountStatus.SUSPENDED
          }
        });

        return { estate, admin };
      });

      await this.audit.record({
        estateId: result.estate.id,
        actorType: 'SYSTEM',
        actorId: 'public-onboarding',
        action: 'ESTATE_SIGNUP_SUBMITTED',
        metadata: {
          estateId: result.estate.id,
          estateName: result.estate.name,
          unitCount: result.estate.unitCount,
          estateType: result.estate.estateType,
          adminUserId: result.admin.id
        }
      });

      await this.audit.record({
        estateId: result.estate.id,
        actorType: 'SYSTEM',
        actorId: 'public-onboarding',
        action: 'PLATFORM_REVIEW_NOTIFICATION_QUEUED',
        metadata: {
          reason: 'Estate admin signup pending approval',
          estateId: result.estate.id
        }
      });

      return {
        estate: result.estate,
        admin: {
          id: result.admin.id,
          name: result.admin.name,
          phone: result.admin.phone,
          email: dto.adminEmail ?? null,
          role: result.admin.role,
          status: result.admin.status
        },
        nextStep: 'GateWise review is required before plan selection and activation.'
      };
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
        throw new ConflictException('A user with this phone number already exists');
      }
      throw error;
    }
  }

  async selectSubscriptionPlan(estateId: string, dto: SelectSubscriptionPlanDto) {
    const estate = await this.getEstateForOnboarding(estateId, dto.adminPhone);
    if (estate.signupStatus !== EstateSignupStatus.APPROVED) {
      throw new BadRequestException('Estate signup must be approved before selecting a plan');
    }

    const plan = await this.prisma.subscriptionPlan.findUnique({ where: { code: dto.planCode } });
    if (!plan || !plan.isActive) {
      throw new NotFoundException('Subscription plan not found');
    }

    await this.assertProviderEnabled(dto.paymentProvider, estateId);

    const updated = await this.prisma.estate.update({
      where: { id: estateId },
      data: {
        subscriptionPlanId: plan.id,
        selectedPaymentProvider: dto.paymentProvider,
        subscriptionStatus: SubscriptionStatus.PLAN_SELECTED
      },
      include: { subscriptionPlan: true }
    });

    await this.audit.record({
      estateId,
      actorType: 'USER',
      actorId: estate.users[0]?.id ?? 'estate-admin',
      action: 'SUBSCRIPTION_PLAN_SELECTED',
      metadata: {
        planCode: plan.code,
        paymentProvider: dto.paymentProvider
      }
    });

    return {
      estate: updated,
      nextStep: 'Initialize a hosted payment checkout, then return for payment confirmation.'
    };
  }

  async recordPayment(estateId: string, dto: RecordEstatePaymentDto) {
    const estate = await this.getEstateForOnboarding(estateId, dto.adminPhone);
    if (estate.signupStatus !== EstateSignupStatus.APPROVED) {
      throw new BadRequestException('Estate signup must be approved before recording payment');
    }

    const plan = dto.planCode
      ? await this.prisma.subscriptionPlan.findUnique({ where: { code: dto.planCode } })
      : estate.subscriptionPlan;
    if (!plan || !plan.isActive) {
      throw new BadRequestException('Select an active subscription plan before recording payment');
    }

    const provider = dto.paymentProvider ?? estate.selectedPaymentProvider;
    if (!provider) {
      throw new BadRequestException('Select a payment provider before recording payment');
    }
    await this.assertProviderEnabled(provider, estateId);
    const amountKobo = this.resolvePaymentAmount(plan, dto.amountKobo);

    try {
      const payment = await this.prisma.$transaction(async (tx) => {
        const record = await tx.estatePayment.create({
          data: {
            estateId,
            planId: plan.id,
            provider,
            amountKobo,
            currency: plan.currency,
            reference: dto.reference,
            status: PaymentStatus.PENDING,
            metadata: this.toJson({
              ...(dto.metadata ?? {}),
              gateway: {
                initialized: false,
                manualReference: true,
                recordedAt: new Date().toISOString()
              }
            })
          },
          include: { plan: true }
        });

        await tx.estate.update({
          where: { id: estateId },
          data: {
            subscriptionPlanId: plan.id,
            selectedPaymentProvider: provider,
            subscriptionStatus: SubscriptionStatus.PAYMENT_PENDING
          }
        });

        return record;
      });

      await this.audit.record({
        estateId,
        actorType: 'USER',
        actorId: estate.users[0]?.id ?? 'estate-admin',
        action: 'ESTATE_PAYMENT_RECORDED',
        metadata: {
          paymentId: payment.id,
          reference: payment.reference,
          provider: payment.provider,
          planCode: payment.plan.code
        }
      });

      return {
        payment,
        nextStep: 'GateWise must confirm the payment before the estate becomes active.'
      };
    } catch (error) {
      if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
        throw new ConflictException('A payment with this reference already exists');
      }
      throw error;
    }
  }

  async initializePayment(estateId: string, dto: InitializeEstatePaymentDto) {
    const estate = await this.getEstateForOnboarding(estateId, dto.adminPhone);
    if (estate.signupStatus !== EstateSignupStatus.APPROVED) {
      throw new BadRequestException('Estate signup must be approved before initializing payment');
    }

    const plan = dto.planCode
      ? await this.prisma.subscriptionPlan.findUnique({ where: { code: dto.planCode } })
      : estate.subscriptionPlan;
    if (!plan || !plan.isActive) {
      throw new BadRequestException('Select an active subscription plan before initializing payment');
    }

    const provider = dto.paymentProvider ?? estate.selectedPaymentProvider;
    if (!provider) {
      throw new BadRequestException('Select a payment provider before initializing payment');
    }
    await this.assertProviderEnabled(provider, estateId);

    const amountKobo = this.resolvePaymentAmount(plan, dto.amountKobo);
    const reference = this.paymentGateway.createReference(provider);
    const admin = estate.users[0];

    let payment = await this.prisma.estatePayment.create({
      data: {
        estateId,
        planId: plan.id,
        provider,
        amountKobo,
        currency: plan.currency,
        reference,
        status: PaymentStatus.PENDING,
        metadata: this.toJson({
          ...(dto.metadata ?? {}),
          returnUrl: dto.returnUrl ?? null,
          gateway: {
            initialized: false,
            provider,
            reference,
            mode: this.paymentGateway.getProviderRuntime(provider).mode,
            createdAt: new Date().toISOString()
          }
        })
      },
      include: { plan: true }
    });

    try {
      const checkout = await this.paymentGateway.initializePayment({
        provider,
        reference,
        amountKobo,
        currency: plan.currency,
        customerEmail: dto.customerEmail,
        customerName: dto.customerName ?? admin?.name ?? estate.name,
        customerPhone: admin?.phone ?? dto.adminPhone,
        estateId,
        estateName: estate.name,
        planCode: plan.code,
        planName: plan.name,
        metadata: {
          ...(dto.metadata ?? {}),
          returnUrl: dto.returnUrl ?? null
        }
      });

      payment = await this.prisma.$transaction(async (tx) => {
        const updated = await tx.estatePayment.update({
          where: { id: payment.id },
          data: {
            metadata: this.toJson({
              ...this.jsonObject(payment.metadata),
              gateway: {
                initialized: true,
                provider,
                mode: checkout.mode,
                reference: checkout.reference,
                providerReference: checkout.providerReference,
                checkoutUrl: checkout.checkoutUrl,
                accessCode: checkout.accessCode ?? null,
                initializedAt: new Date().toISOString(),
                raw: checkout.raw
              }
            })
          },
          include: { plan: true }
        });

        await tx.estate.update({
          where: { id: estateId },
          data: {
            subscriptionPlanId: plan.id,
            selectedPaymentProvider: provider,
            subscriptionStatus: SubscriptionStatus.PAYMENT_PENDING
          }
        });

        return updated;
      });

      await this.audit.record({
        estateId,
        actorType: 'USER',
        actorId: admin?.id ?? 'estate-admin',
        action: 'ESTATE_PAYMENT_INITIALIZED',
        metadata: {
          paymentId: payment.id,
          reference: payment.reference,
          provider,
          planCode: plan.code,
          gatewayMode: checkout.mode
        }
      });

      return {
        payment,
        checkout: {
          provider,
          mode: checkout.mode,
          reference: checkout.reference,
          providerReference: checkout.providerReference,
          checkoutUrl: checkout.checkoutUrl,
          accessCode: checkout.accessCode ?? null
        },
        nextStep: 'Open checkoutUrl, complete payment, then wait for GateWise confirmation.'
      };
    } catch (error) {
      await this.prisma.estatePayment.update({
        where: { id: payment.id },
        data: {
          status: PaymentStatus.FAILED,
          metadata: this.toJson({
            ...this.jsonObject(payment.metadata),
            gateway: {
              ...this.jsonObject(this.jsonObject(payment.metadata).gateway),
              initialized: false,
              failedAt: new Date().toISOString(),
              error: error instanceof Error ? error.message : String(error)
            }
          })
        }
      });
      throw error;
    }
  }

  async handlePaymentCallback(query: Record<string, unknown>) {
    const reference =
      this.queryString(query.reference) ??
      this.queryString(query.tx_ref) ??
      this.queryString(query.trxref) ??
      this.queryString(query.paymentReference);
    if (!reference) {
      return {
        status: 'received',
        nextStep: 'Payment gateway callback did not include a reference. Check the gateway dashboard for the transaction.'
      };
    }

    const payment = await this.prisma.estatePayment.findUnique({ where: { reference } });
    if (!payment) {
      return {
        status: 'received',
        reference,
        nextStep: 'Payment callback was received, but no local payment record matched this reference.'
      };
    }

    const providerTransactionId =
      this.queryString(query.transaction_id) ?? this.queryString(query.session_id) ?? this.queryString(query.transactionReference);
    const metadata = this.jsonObject(payment.metadata);
    const gateway = this.jsonObject(metadata.gateway);
    const updated = await this.prisma.estatePayment.update({
      where: { id: payment.id },
      data: {
        metadata: this.toJson({
          ...metadata,
          gateway: {
            ...gateway,
            providerTransactionId: providerTransactionId ?? gateway.providerTransactionId ?? null,
            lastCallbackAt: new Date().toISOString(),
            lastCallback: query
          }
        })
      }
    });

    return {
      status: 'received',
      payment: {
        id: updated.id,
        estateId: updated.estateId,
        provider: updated.provider,
        reference: updated.reference,
        status: updated.status
      },
        nextStep: 'Payment callback stored. GateWise should confirm the payment to activate the estate.'
    };
  }

  private async getEstateForOnboarding(estateId: string, adminPhone: string) {
    const estate = await this.prisma.estate.findUnique({
      where: { id: estateId },
      include: {
        subscriptionPlan: true,
        users: {
          where: { phone: adminPhone, role: UserRole.ESTATE_ADMIN },
          take: 1
        }
      }
    });
    if (!estate || estate.users.length === 0) {
      throw new NotFoundException('Estate signup was not found for this admin phone');
    }
    if (estate.status === EstateStatus.REJECTED || estate.status === EstateStatus.SUSPENDED) {
      throw new ForbiddenException('Estate signup is not eligible for onboarding changes');
    }
    return estate;
  }

  private async assertProviderEnabled(provider: PaymentProvider, estateId: string) {
    const [global, override] = await Promise.all([
      this.prisma.globalPaymentProviderSetting.findUnique({ where: { provider } }),
      this.prisma.estatePaymentProviderSetting.findUnique({
        where: { estateId_provider: { estateId, provider } }
      })
    ]);

    const enabled = override?.enabled ?? global?.enabled ?? false;
    if (!enabled) {
      throw new BadRequestException(`${provider} is not enabled for this estate`);
    }
  }

  private resolvePaymentAmount(plan: { monthlyPriceKobo: number | null; customPricing: boolean }, amountKobo?: number) {
    const amount = amountKobo ?? plan.monthlyPriceKobo;
    if (!amount || amount <= 0) {
      if (plan.customPricing) {
        throw new BadRequestException('Custom-priced plans require amountKobo before payment can be recorded');
      }
      throw new BadRequestException('Subscription plan price is not configured');
    }
    return amount;
  }

  private jsonObject(value: unknown): Record<string, unknown> {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
      return {};
    }
    return value as Record<string, unknown>;
  }

  private toJson(value: Record<string, unknown>): Prisma.InputJsonValue {
    return JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
  }

  private queryString(value: unknown) {
    if (typeof value === 'string' && value.length > 0) return value;
    if (Array.isArray(value) && typeof value[0] === 'string' && value[0].length > 0) return value[0];
    return null;
  }
}
