import { stripe, MIN_CHARGE_CENTS } from './stripe';
import { db, schema } from './db';
import { eq, and, inArray, gte, lt, desc } from 'drizzle-orm';
import { grossEurCentsFromTokens } from './conversion-pricing';
import { maybeResetAnnualLicenseCredits, suspendCompany, reactivateCompany, getCompanyById } from './companies';
import { issueCompanyInvoice, buildEntity } from './fatture-in-cloud';
import type Stripe from 'stripe';

/** One billed conversion on a company's monthly invoice. */
export type InvoiceLineItem = {
  projectId: string;
  name: string;
  sourceLanguage: string | null;
  targetLanguage: string | null;
  completedAt: string | null;
  /** List price before license credit. */
  grossCents: number;
  /** Portion of this line covered by annual license credit. */
  creditCents: number;
  /** Net amount billed for this line (gross − credit). */
  amountCents: number;
};

/**
 * Build the per-conversion line items for a company in [periodStart, periodEnd).
 * One line per completed conversion showing gross, license credit applied, and
 * net billed, plus an aggregate line for any extra-slot purchases.
 */
export async function calculateCompanyInvoice(
  companyId: string,
  periodStart: Date,
  periodEnd: Date,
): Promise<{
  items: InvoiceLineItem[];
  slotChargesCents: number;
  grossCents: number;
  creditCents: number;
  conversionCents: number;
  amountCents: number;
  billableTokens: number;
}> {
  const companyUsers = await db.query.users.findMany({
    columns: { id: true },
    where: eq(schema.users.companyId, companyId),
  });
  const userIds = companyUsers.map((u) => u.id);
  if (userIds.length === 0) {
    return { items: [], slotChargesCents: 0, grossCents: 0, creditCents: 0, conversionCents: 0, amountCents: 0, billableTokens: 0 };
  }

  const completed = await db.query.projects.findMany({
    where: and(inArray(schema.projects.userId, userIds), eq(schema.projects.status, 'completed')),
    columns: {
      id: true,
      name: true,
      sourceLanguage: true,
      targetLanguage: true,
      approvedGrossEurCents: true,
      approvedNetEurCents: true,
      approvedEstimatedTokens: true,
      tokensUsed: true,
      completedAt: true,
      updatedAt: true,
    },
  });

  const ps = periodStart.getTime();
  const pe = periodEnd.getTime();

  const items: InvoiceLineItem[] = [];
  let grossTotal = 0;
  let creditTotal = 0;
  let netTotal = 0;
  let billableTokens = 0;

  for (const p of completed) {
    const tsRaw = p.completedAt ?? p.updatedAt;
    if (!tsRaw) continue;
    const t = new Date(tsRaw).getTime();
    if (t < ps || t >= pe) continue;

    // Gross list price, then net after the license credit applied at approval.
    const gross = p.approvedGrossEurCents
      ?? p.approvedNetEurCents
      ?? grossEurCentsFromTokens(p.tokensUsed ?? 0);
    if (gross <= 0) continue;
    const net = p.approvedNetEurCents ?? gross;
    const credit = Math.max(0, gross - net);

    grossTotal += gross;
    creditTotal += credit;
    netTotal += net;
    billableTokens += p.approvedEstimatedTokens ?? p.tokensUsed ?? 0;
    items.push({
      projectId: p.id,
      name: p.name,
      sourceLanguage: p.sourceLanguage,
      targetLanguage: p.targetLanguage,
      completedAt: tsRaw ? new Date(tsRaw).toISOString() : null,
      grossCents: gross,
      creditCents: credit,
      amountCents: net,
    });
  }

  const slotRows = await db.query.slotPurchases.findMany({
    where: and(
      eq(schema.slotPurchases.companyId, companyId),
      gte(schema.slotPurchases.purchasedAt, periodStart),
      lt(schema.slotPurchases.purchasedAt, periodEnd),
    ),
    columns: { totalCents: true },
  });
  const slotChargesCents = slotRows.reduce((sum, r) => sum + r.totalCents, 0);

  return {
    items,
    slotChargesCents,
    grossCents: grossTotal,
    creditCents: creditTotal,
    conversionCents: netTotal,
    amountCents: netTotal + slotChargesCents,
    billableTokens,
  };
}

// ── Customer management ────────────────────────────────────────────────────

export async function getOrCreateStripeCustomer(
  userId: string,
  email: string,
  name?: string | null,
): Promise<string> {
  const user = await db.query.users.findFirst({ where: eq(schema.users.id, userId) });

  if (user?.stripeCustomerId) {
    return user.stripeCustomerId;
  }

  const customer = await stripe.customers.create({
    email,
    name: name ?? undefined,
    metadata: { userId },
  });

  await db.update(schema.users)
    .set({ stripeCustomerId: customer.id, updatedAt: new Date() })
    .where(eq(schema.users.id, userId));

  return customer.id;
}

/**
 * Sum approved upfront € for conversions completed in [periodStart, periodEnd).
 * Uses snapshot columns when present; otherwise falls back to flat-rate estimate from tokensUsed (admin / legacy).
 */
export async function calculateCompanyMonthlyCharge(
  companyId: string,
  periodStart: Date,
  periodEnd: Date,
): Promise<{ totalTokens: number; billableTokens: number; freeTokensApplied: number; amountCents: number; slotChargesCents: number }> {
  const companyUsers = await db.query.users.findMany({
    columns: { id: true },
    where: eq(schema.users.companyId, companyId),
  });

  const userIds = companyUsers.map(u => u.id);
  if (userIds.length === 0) return { totalTokens: 0, billableTokens: 0, freeTokensApplied: 0, amountCents: 0, slotChargesCents: 0 };

  const completed = await db.query.projects.findMany({
    where: and(inArray(schema.projects.userId, userIds), eq(schema.projects.status, 'completed')),
    columns: {
      approvedNetEurCents: true,
      approvedEstimatedTokens: true,
      tokensUsed: true,
      completedAt: true,
      updatedAt: true,
    },
  });

  let conversionCents = 0;
  let totalTokens = 0;

  const ps = periodStart.getTime();
  const pe = periodEnd.getTime();

  for (const p of completed) {
    const tsRaw = p.completedAt ?? p.updatedAt;
    if (!tsRaw) continue;
    const t = new Date(tsRaw).getTime();
    if (t < ps || t >= pe) continue;

    if (p.approvedNetEurCents != null) {
      conversionCents += p.approvedNetEurCents;
      totalTokens += p.approvedEstimatedTokens ?? p.tokensUsed ?? 0;
    } else {
      const tu = p.tokensUsed ?? 0;
      if (tu <= 0) continue;
      conversionCents += grossEurCentsFromTokens(tu);
      totalTokens += tu;
    }
  }

  // Add extra slot purchase costs for the period
  const slotRows = await db.query.slotPurchases.findMany({
    where: and(
      eq(schema.slotPurchases.companyId, companyId),
      gte(schema.slotPurchases.purchasedAt, periodStart),
      lt(schema.slotPurchases.purchasedAt, periodEnd),
    ),
    columns: { totalCents: true },
  });
  const slotChargesCents = slotRows.reduce((sum, r) => sum + r.totalCents, 0);

  const amountCents = conversionCents + slotChargesCents;
  return { totalTokens, billableTokens: totalTokens, freeTokensApplied: 0, amountCents, slotChargesCents };
}

// ── Monthly billing for one company owner ─────────────────────────────────

/**
 * Bill the owner for all conversions completed last calendar month (approved upfront €).
 * Plan credits were already applied at approval time — not reapplied here.
 */
export async function processOwnerMonthlyBilling(
  ownerId: string,
  companyId: string,
  periodStart: Date,
  periodEnd: Date,
): Promise<{ status: 'charged' | 'skipped' | 'no_payment_method' | 'failed'; amountCents: number }> {
  const user = await db.query.users.findFirst({ where: eq(schema.users.id, ownerId) });
  if (!user) return { status: 'skipped', amountCents: 0 };

  // Always check the most recent record for this period (retries can leave multiple rows).
  const existing = await db.query.billingRecords.findFirst({
    where: and(
      eq(schema.billingRecords.userId, ownerId),
      eq(schema.billingRecords.periodStart, periodStart),
    ),
    orderBy: [desc(schema.billingRecords.createdAt)],
  });
  if (existing && existing.status !== 'failed') {
    return { status: 'skipped', amountCents: existing.amountCents };
  }

  const { billableTokens, amountCents } = await calculateCompanyMonthlyCharge(companyId, periodStart, periodEnd);

  const recordId = crypto.randomUUID();

  try {
    if (amountCents < MIN_CHARGE_CENTS) {
      await db.insert(schema.billingRecords).values({
        id: recordId,
        userId: ownerId,
        periodStart,
        periodEnd,
        totalTokens: billableTokens,
        amountCents,
        status: 'skipped',
      });
      await maybeResetAnnualLicenseCredits(companyId, periodEnd);
      return { status: 'skipped', amountCents };
    }

    const customerId = await getOrCreateStripeCustomer(ownerId, user.email, user.name);

    const customer = await stripe.customers.retrieve(customerId) as Stripe.Customer;
    const rawPm = customer.invoice_settings?.default_payment_method;
    const paymentMethodId = typeof rawPm === 'string'
      ? rawPm
      : (rawPm as Stripe.PaymentMethod | null)?.id ?? null;

    if (!paymentMethodId) {
      await db.insert(schema.billingRecords).values({
        id: recordId,
        userId: ownerId,
        periodStart,
        periodEnd,
        totalTokens: billableTokens,
        amountCents,
        status: 'failed',
        errorMessage: 'No default payment method on file',
      });
      // Credits are NOT reset — the outstanding charge must be resolved first.
      await suspendCompany(companyId, 'No payment method on file for the monthly invoice.');
      return { status: 'no_payment_method', amountCents };
    }

    const paymentIntent = await stripe.paymentIntents.create({
      amount: amountCents,
      currency: 'eur',
      customer: customerId,
      payment_method: paymentMethodId,
      confirm: true,
      off_session: true,
      description: `Scriba conversions — ${periodStart.toISOString().slice(0, 7)}`,
      metadata: {
        ownerId,
        companyId,
        periodStart: periodStart.toISOString(),
        totalEstimatedTokens: String(billableTokens),
      },
    });

    await db.insert(schema.billingRecords).values({
      id: recordId,
      userId: ownerId,
      periodStart,
      periodEnd,
      totalTokens: billableTokens,
      amountCents,
      status: 'charged',
      stripePaymentIntentId: paymentIntent.id,
    });

    // Payment went through — lift any prior suspension. License credits are an
    // annual allowance, so only renew them on the license anniversary month.
    await reactivateCompany(companyId);
    await maybeResetAnnualLicenseCredits(companyId, periodEnd);

    // Issue the monthly invoice with one line per translated conversion.
    // Best-effort: never let an invoicing hiccup fail an otherwise successful charge.
    try {
      await issueMonthlyInvoice(companyId, ownerId, periodStart, periodEnd, recordId);
    } catch (invErr) {
      console.error('[billing] invoice issuance failed (charge succeeded):', invErr);
    }

    return { status: 'charged', amountCents };
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    await db.insert(schema.billingRecords).values({
      id: recordId,
      userId: ownerId,
      periodStart,
      periodEnd,
      totalTokens: billableTokens,
      amountCents,
      status: 'failed',
      errorMessage: message,
    }).catch(() => {});  // Suppress PK collision if record was already inserted above.
    // Credits are NOT reset — the outstanding charge must be resolved first.
    await suspendCompany(companyId, `Last monthly payment failed: ${message}`);
    return { status: 'failed', amountCents };
  }
}

/**
 * Issue the Fatture in Cloud invoice for a company's billing period and persist
 * the returned document id on the billing record. One line per translated
 * conversion; an aggregate line covers any extra-slot purchases.
 */
async function issueMonthlyInvoice(
  companyId: string,
  ownerId: string,
  periodStart: Date,
  periodEnd: Date,
  recordId: string,
): Promise<void> {
  const company = await getCompanyById(companyId);
  if (!company) return;
  const owner = await db.query.users.findFirst({ where: eq(schema.users.id, ownerId) });
  if (!owner) return;

  const invoice = await calculateCompanyInvoice(companyId, periodStart, periodEnd);
  if (invoice.amountCents <= 0) return;

  const fin = (company.financialData ?? {}) as {
    billingAddress?: string;
    billingEmail?: string;
    vatNumber?: string;
    taxId?: string;
  };

  const entity = buildEntity({
    companyName: company.name,
    userName: owner.name,
    email: fin.billingEmail || owner.email,
    vatNumber: fin.vatNumber,
    taxId: fin.taxId,
    address: fin.billingAddress,
  });

  const fmtLang = (s: string | null) => (s ?? '').toUpperCase();
  const lines = invoice.items.map((it) => ({
    name: `${it.name} (${fmtLang(it.sourceLanguage)} → ${fmtLang(it.targetLanguage)})`,
    amountEur: it.amountCents / 100,
  }));
  if (invoice.slotChargesCents > 0) {
    lines.push({ name: 'Extra conversion slots', amountEur: invoice.slotChargesCents / 100 });
  }

  const period = periodStart.toISOString().slice(0, 7);
  const docId = await issueCompanyInvoice({
    entity,
    date: periodEnd.toISOString().slice(0, 10),
    lines,
    amountCents: invoice.amountCents,
    notes: `Scriba conversions — ${period}`,
  });

  await db
    .update(schema.billingRecords)
    .set({ fattureInCloudDocumentId: String(docId) })
    .where(eq(schema.billingRecords.id, recordId))
    .catch(() => {});
}

// ── Run billing for all companies (called by cron on 1st of month) ─────────

export async function runMonthlyBillingForAll(): Promise<{
  processed: number;
  charged: number;
  skipped: number;
  failed: number;
  noPaymentMethod: number;
}> {
  const now = new Date();
  const periodStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
  const periodEnd   = new Date(now.getFullYear(), now.getMonth(), 1);

  const owners = await db.query.users.findMany({
    columns: { id: true, companyId: true },
    where: eq(schema.users.isOwner, true),
  });

  let charged = 0, skipped = 0, failed = 0, noPaymentMethod = 0;

  for (const owner of owners) {
    if (!owner.companyId) continue;
    try {
      const result = await processOwnerMonthlyBilling(owner.id, owner.companyId, periodStart, periodEnd);
      if (result.status === 'charged') charged++;
      else if (result.status === 'failed') failed++;
      else if (result.status === 'no_payment_method') noPaymentMethod++;
      else skipped++;
    } catch (err) {
      console.error(`[billing] Unhandled error for owner ${owner.id}:`, err);
      failed++;
    }
  }

  return { processed: owners.length, charged, skipped, failed, noPaymentMethod };
}
