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 = new Stripe(process.env.STRIPE_SECRET_KEY as string);

  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,
  ) {}

  // ═══════════════════════════════════════════════════════════════
  // 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,
    });
    return {
      active: await this.hasActiveSubscription(companyId),
      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);

    // 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/billing/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${base}/client/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)');
    }
    const sub = await this.getOrCreateRow(companyId);
    sub.price_amount = Math.round(amount);
    sub.price_currency = (currency || process.env.STRIPE_CURRENCY || 'aed').toLowerCase();
    sub.price_interval = interval || 'year';
    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);
  }

  /** 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) {
      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}`);
    }

    switch (event.type) {
      case 'checkout.session.completed': {
        const s = event.data.object as Stripe.Checkout.Session;
        const companyId = Number(s.metadata?.company_id);
        await this.activate(companyId, {
          subscriptionId: (s.subscription as string) ?? null,
          amount: s.amount_total ?? null,
          currency: s.currency ?? undefined,
        });
        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) {
      sub = this.subRepo.create({
        company_id: companyId,
        status: 'incomplete',
        currency: process.env.STRIPE_CURRENCY ?? 'aed',
      } as any) as unknown as ClientSubscription;
      await this.subRepo.save(sub);
    }
    return sub;
  }

  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;
    const cpe = (s as any).current_period_end;
    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}`);
  }
}
