import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { InjectRepository, InjectDataSource } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import Stripe from 'stripe';
import { ClientSubscription } from './entities/client-subscription.entity';
import { Company } from '../../companies/entities/company.entity';

@Injectable()
export class BillingService {
  private readonly logger = new Logger('BillingService');

  // apiVersion omitted -> uses your Stripe account default.
  private readonly stripe: Stripe;

  constructor(
    @InjectRepository(ClientSubscription, 'scheme_dbs')
    private readonly subRepo: Repository<ClientSubscription>,
    @InjectRepository(Company, 'scheme_dbs')
    private readonly companyRepo: Repository<Company>,
    @InjectDataSource('scheme_dbs')
    private readonly dataSource: DataSource,
  ) {
    // HARDENING #6: fail loudly at boot if the key is missing, instead of a
    // mysterious 500 on the first checkout. apiVersion still omitted to keep
    // your existing account-default behavior.
    const key = process.env.STRIPE_SECRET_KEY;
    if (!key) {
      throw new Error('STRIPE_SECRET_KEY is not set — billing cannot start');
    }
    this.stripe = new Stripe(key);
  }

  // ═══════════════════════════════════════════════════════════════
  // THE GATE PREDICATE (used by the login gate later)
  // ═══════════════════════════════════════════════════════════════
  async hasActiveSubscription(companyId: number): Promise<boolean> {
    if (!companyId) return false;
    const rows = await this.dataSource.query(
      `SELECT id FROM client_subscriptions
         WHERE company_id = ? AND status = 'active'
           AND (current_period_end IS NULL OR current_period_end > NOW())
         LIMIT 1`,
      [companyId],
    );
    return Array.isArray(rows) && rows.length > 0;
  }

  async getStatus(companyId: number) {
    const sub = await this.subRepo.findOne({
      where: { company_id: companyId } as any,
    });
    const active = await this.hasActiveSubscription(companyId);
    const billingEnabled = !!sub?.billing_enabled;
    return {
      active,
      // ── per-company paywall switch + derived gate flags ──
      billing_enabled: billingEnabled,
      requires_payment: billingEnabled && !active, // client portal shows the gateway
      access_allowed: !billingEnabled || active, // client may enter dashboard/app
      status: sub?.status ?? 'none',
      plan: sub?.plan ?? null,
      is_free: sub?.plan === 'comp',
      current_period_end: sub?.current_period_end ?? null,
      // admin-set price for the CRM to show/edit:
      price_amount: sub?.price_amount ?? null,
      price_currency: sub?.price_currency ?? process.env.STRIPE_CURRENCY ?? 'aed',
      price_interval: sub?.price_interval ?? null,
      currency: sub?.currency ?? process.env.STRIPE_CURRENCY ?? 'aed',
    };
  }

  // ═══════════════════════════════════════════════════════════════
  // CLIENT — start Stripe Checkout (uses the admin-set price if any)
  // ═══════════════════════════════════════════════════════════════
  async createCheckoutSession(
    companyId: number,
    clientUserId: number,
    email?: string,
  ) {
    if (!companyId) throw new BadRequestException('No company in token');

    const company = await this.companyRepo.findOne({
      where: { id: companyId } as any,
    });
    if (!company) throw new BadRequestException('Company not found');

    const sub = await this.getOrCreateRow(companyId);

    // ── PER-COMPANY GATE ──────────────────────────────────────────
    // A company can only pay if superadmin enabled billing for it. This
    // stops any client from creating a Stripe charge unless you turned
    // the switch on for their company.
    if (!sub.billing_enabled) {
      throw new BadRequestException('Billing is not enabled for this company');
    }

    // Ensure a Stripe customer exists for this company
    let customerId = sub.stripe_customer_id ?? null;
    if (!customerId) {
      const customer = await this.stripe.customers.create({
        email,
        name: (company as any).name,
        metadata: { company_id: String(companyId) },
      });
      customerId = customer.id;
      sub.stripe_customer_id = customerId;
    }

    // Decide the line item:
    //  - if the admin set a per-company price -> dynamic price_data
    //  - otherwise -> the global STRIPE_PRICE_ID
    let sessionMode: 'subscription' | 'payment';
    let lineItem: any;

    if (sub.price_amount && sub.price_amount > 0) {
      const interval = sub.price_interval || 'year'; // 'year' | 'month' | 'one_time'
      sessionMode = interval === 'one_time' ? 'payment' : 'subscription';
      const priceData: any = {
        currency: sub.price_currency || process.env.STRIPE_CURRENCY || 'aed',
        product_data: { name: 'Client Portal Access' },
        unit_amount: sub.price_amount, // minor units
      };
      if (sessionMode === 'subscription') {
        priceData.recurring = { interval }; // 'year' | 'month'
      }
      lineItem = { price_data: priceData, quantity: 1 };
    } else {
      const priceId = process.env.STRIPE_PRICE_ID as string;
      if (!priceId) {
        throw new BadRequestException(
          'No price set for this company and STRIPE_PRICE_ID not configured',
        );
      }
      sessionMode =
        process.env.STRIPE_MODE === 'payment' ? 'payment' : 'subscription';
      lineItem = { price: priceId, quantity: 1 };
    }

    const base = process.env.CLIENT_PORTAL_URL || 'http://localhost:3008';
    const session = await this.stripe.checkout.sessions.create({
      mode: sessionMode,
      customer: customerId,
      line_items: [lineItem],
      success_url: `${base}/client/dashboard?billing=success&session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${base}/client/dashboard?billing=cancel`,
      metadata: {
        company_id: String(companyId),
        client_user_id: String(clientUserId),
      },
    });

    sub.stripe_checkout_session_id = session.id;
    if (!sub.client_user_id) sub.client_user_id = clientUserId;
    await this.subRepo.save(sub);

    return { url: session.url, session_id: session.id };
  }

  // ═══════════════════════════════════════════════════════════════
  // ADMIN (superadmin) actions
  // ═══════════════════════════════════════════════════════════════

  /** Set the price THIS company pays. amount = minor units (50000 = 500.00). */
  async setCompanyPrice(
    companyId: number,
    amount: number,
    currency?: string,
    interval?: string,
  ) {
    if (!amount || amount <= 0) {
      throw new BadRequestException('amount must be a positive integer (minor units)');
    }
    // HARDENING #9: validate currency + interval so a typo can't reach Stripe
    // and blow up at checkout time. Extend these sets as your business needs.
    const ALLOWED_CCY = new Set(['aed', 'usd', 'eur', 'gbp', 'sar']);
    const ALLOWED_INT = new Set(['year', 'month', 'one_time']);
    const ccy = (currency || process.env.STRIPE_CURRENCY || 'aed').toLowerCase();
    if (!ALLOWED_CCY.has(ccy)) {
      throw new BadRequestException(`Unsupported currency: ${ccy}`);
    }
    const intv = interval || 'year';
    if (!ALLOWED_INT.has(intv)) {
      throw new BadRequestException(`Unsupported interval: ${intv}`);
    }
    const sub = await this.getOrCreateRow(companyId);
    sub.price_amount = Math.round(amount);
    sub.price_currency = ccy;
    sub.price_interval = intv;
    await this.subRepo.save(sub);
    this.logger.log(`Admin set price for company ${companyId}: ${amount} ${sub.price_currency}/${sub.price_interval}`);
    return this.getStatus(companyId);
  }

  /** Superadmin: turn the paywall ON/OFF for a company (the per-company enable). */
  async setBillingEnabled(companyId: number, enabled: boolean) {
    const sub = await this.getOrCreateRow(companyId);
    sub.billing_enabled = enabled;
    await this.subRepo.save(sub);
    this.logger.log(
      `Admin ${enabled ? 'ENABLED' : 'DISABLED'} billing for company ${companyId}`,
    );
    return this.getStatus(companyId);
  }

  /** Give a company FREE access (comp) — no Stripe, no charge. */
  async grantFreeAccess(companyId: number) {
    const sub = await this.getOrCreateRow(companyId);
    sub.status = 'active';
    sub.plan = 'comp';
    sub.current_period_end = null; // lifetime
    await this.subRepo.save(sub);
    this.logger.log(`Admin GRANTED FREE access to company ${companyId}`);
    return this.getStatus(companyId);
  }

  /** Mark a company as paid manually (offline / bank transfer). */
  async markPaidManually(companyId: number) {
    const sub = await this.getOrCreateRow(companyId);
    sub.status = 'active';
    sub.plan = 'manual';
    await this.subRepo.save(sub);
    this.logger.log(`Admin marked company ${companyId} as PAID (manual)`);
    return this.getStatus(companyId);
  }

  /** Revoke access (paywall returns on next check). */
  async revokeAccess(companyId: number) {
    const sub = await this.subRepo.findOne({
      where: { company_id: companyId } as any,
    });
    if (sub) {
      // HARDENING #2: if a live Stripe subscription exists, cancel it at Stripe
      // too — otherwise the customer's card keeps getting charged after revoke.
      if (sub.stripe_subscription_id) {
        try {
          await this.stripe.subscriptions.cancel(sub.stripe_subscription_id);
          this.logger.warn(`Stripe subscription canceled for company ${companyId}`);
        } catch (e: any) {
          this.logger.warn(`Stripe cancel failed for ${companyId}: ${e.message}`);
        }
      }
      sub.status = 'canceled';
      await this.subRepo.save(sub);
    }
    this.logger.warn(`Admin REVOKED access for company ${companyId}`);
    return this.getStatus(companyId);
  }

  // ═══════════════════════════════════════════════════════════════
  // STRIPE WEBHOOK
  // ═══════════════════════════════════════════════════════════════
  async handleWebhook(rawBody: Buffer | string, signature: string) {
    const secret = process.env.STRIPE_WEBHOOK_SECRET as string;
    let event: Stripe.Event;
    try {
      event = this.stripe.webhooks.constructEvent(rawBody, signature, secret);
    } catch (err: any) {
      this.logger.error(`Webhook signature failed: ${err.message}`);
      throw new BadRequestException(`Webhook Error: ${err.message}`);
    }

    // HARDENING #3: idempotency — Stripe retries events and may deliver the
    // same event.id more than once. Process each exactly once.
    try {
      const already = await this.dataSource.query(
        `SELECT id FROM stripe_events WHERE event_id = ? LIMIT 1`,
        [event.id],
      );
      if (Array.isArray(already) && already.length) {
        this.logger.debug(`Duplicate event ${event.id} ignored`);
        return { received: true };
      }
      await this.dataSource.query(
        `INSERT INTO stripe_events (event_id, type) VALUES (?, ?)`,
        [event.id, event.type],
      );
    } catch (e: any) {
      // If the stripe_events table isn't created yet, don't block processing.
      this.logger.warn(`stripe_events dedup skipped: ${e.message}`);
    }

    switch (event.type) {
      case 'checkout.session.completed': {
        const s = event.data.object as Stripe.Checkout.Session;
        const companyId = Number(s.metadata?.company_id);
        // HARDENING #5: for async payment methods this event can fire before
        // the payment settles — only activate when actually paid.
        if (s.payment_status && s.payment_status !== 'paid') break;
        await this.activate(companyId, {
          subscriptionId: (s.subscription as string) ?? null,
          amount: s.amount_total ?? null,
          currency: s.currency ?? undefined,
        });
        // HARDENING #5: set the period end immediately for subscriptions, so a
        // yearly sub isn't treated as lifetime if invoice.paid isn't enabled.
        if (s.mode === 'subscription' && s.subscription) {
          try {
            const full = await this.stripe.subscriptions.retrieve(
              s.subscription as string,
            );
            await this.setStatusFromStripe(companyId, full);
          } catch (e: any) {
            this.logger.warn(
              `Could not retrieve subscription for period end: ${e.message}`,
            );
          }
        }
        break;
      }
      case 'invoice.paid':
      case 'customer.subscription.updated': {
        const sub = event.data.object as Stripe.Subscription;
        const companyId = await this.companyIdFromCustomer(sub.customer as string);
        if (companyId) await this.setStatusFromStripe(companyId, sub);
        break;
      }
      case 'invoice.payment_failed': {
        const inv = event.data.object as Stripe.Invoice;
        const companyId = await this.companyIdFromCustomer(inv.customer as string);
        if (companyId) await this.markStatus(companyId, 'past_due');
        break;
      }
      case 'customer.subscription.deleted': {
        const sub = event.data.object as Stripe.Subscription;
        const companyId = await this.companyIdFromCustomer(sub.customer as string);
        if (companyId) await this.markStatus(companyId, 'canceled');
        break;
      }
      default:
        this.logger.debug(`Unhandled event: ${event.type}`);
    }
    return { received: true };
  }

  // ── helpers ─────────────────────────────────────────────
  private async getOrCreateRow(companyId: number): Promise<ClientSubscription> {
    let sub = await this.subRepo.findOne({
      where: { company_id: companyId } as any,
    });
    if (sub) return sub;
    try {
      sub = this.subRepo.create({
        company_id: companyId,
        status: 'incomplete',
        currency: process.env.STRIPE_CURRENCY ?? 'aed',
      } as any) as unknown as ClientSubscription;
      return await this.subRepo.save(sub);
    } catch {
      // HARDENING #7: lost a concurrent race on the unique company_id index —
      // the row now exists, so read and return it instead of throwing a 500.
      const existing = await this.subRepo.findOne({
        where: { company_id: companyId } as any,
      });
      if (existing) return existing;
      throw new Error(`Failed to create subscription row for company ${companyId}`);
    }
  }

  private async companyIdFromCustomer(
    customerId: string,
  ): Promise<number | null> {
    const rows = await this.dataSource.query(
      `SELECT company_id FROM client_subscriptions
         WHERE stripe_customer_id = ? LIMIT 1`,
      [customerId],
    );
    return rows?.[0]?.company_id ? Number(rows[0].company_id) : null;
  }

  private async activate(
    companyId: number,
    d: { subscriptionId: string | null; amount: number | null; currency?: string },
  ) {
    if (!companyId) return;
    const sub = await this.subRepo.findOne({
      where: { company_id: companyId } as any,
    });
    if (!sub) return;
    sub.status = 'active';
    if (sub.plan !== 'comp' && sub.plan !== 'manual') sub.plan = 'portal_access';
    if (d.subscriptionId) sub.stripe_subscription_id = d.subscriptionId;
    if (d.amount != null) sub.amount = d.amount;
    if (d.currency) sub.currency = d.currency;
    await this.subRepo.save(sub);
    this.logger.log(`✓ Subscription ACTIVE for company ${companyId}`);
  }

  private async setStatusFromStripe(companyId: number, s: Stripe.Subscription) {
    const sub = await this.subRepo.findOne({
      where: { company_id: companyId } as any,
    });
    if (!sub) return;
    const map: Record<string, ClientSubscription['status']> = {
      active: 'active',
      trialing: 'active',
      past_due: 'past_due',
      canceled: 'canceled',
      unpaid: 'past_due',
      incomplete: 'incomplete',
      incomplete_expired: 'expired',
    };
    sub.status = map[s.status] ?? 'incomplete';
    sub.stripe_subscription_id = s.id;
    // HARDENING #4: newer Stripe API versions moved current_period_end onto the
    // subscription ITEM. Read both locations so a paid sub isn't mistaken for
    // a lifetime (null) sub and wrongly kept active forever.
    const cpe =
      (s as any).current_period_end ??
      (s as any).items?.data?.[0]?.current_period_end ??
      null;
    sub.current_period_end = cpe ? new Date(cpe * 1000) : null;
    await this.subRepo.save(sub);
    this.logger.log(`Subscription ${sub.status} for company ${companyId}`);
  }

  private async markStatus(
    companyId: number,
    status: ClientSubscription['status'],
  ) {
    await this.dataSource.query(
      `UPDATE client_subscriptions SET status = ?, updated_at = NOW() WHERE company_id = ?`,
      [status, companyId],
    );
    this.logger.warn(`Subscription ${status} for company ${companyId}`);
  }
}
