<?php

declare(strict_types=1);

final class Billing
{
    public static function ensureInvoicesForMonth(PDO $pdo, string $billingMonth): array
    {
        if (!preg_match('/^\d{4}-\d{2}$/', $billingMonth)) {
            throw new InvalidArgumentException('Invalid billing_month');
        }

        $pdo->beginTransaction();
        try {
            $subs = $pdo->query(
                "SELECT s.id AS subscription_id, s.learner_id, COALESCE(s.guardian_id, l.guardian_id) AS guardian_id, s.course_session_id,
                        s.start_date, s.end_date, s.monthly_rate_override,
                        cs.monthly_rate, cs.billing_day
                 FROM subscriptions s
                 JOIN learners l ON l.id = s.learner_id
                 JOIN course_sessions cs ON cs.id = s.course_session_id
                 WHERE s.status = 'active'"
            )->fetchAll();

            $created = 0;
            $skipped = 0;

            foreach ($subs as $sub) {
                $existsStmt = $pdo->prepare('SELECT id FROM invoices WHERE subscription_id = ? AND billing_month = ? LIMIT 1');
                $existsStmt->execute([(int) $sub['subscription_id'], $billingMonth]);
                if ($existsStmt->fetchColumn()) {
                    $skipped++;
                    continue;
                }

                $base = $sub['monthly_rate_override'] !== null ? (float) $sub['monthly_rate_override'] : (float) $sub['monthly_rate'];

                $discountAmount = self::discountAmount($pdo, (int) $sub['subscription_id'], $billingMonth, $base);
                $discountAmount = max(0.0, min($discountAmount, $base));
                $due = $base - $discountAmount;

                $day = (int) $sub['billing_day'];
                $day = max(1, min(28, $day));
                $dueDate = $billingMonth . '-' . str_pad((string) $day, 2, '0', STR_PAD_LEFT);

                $ins = $pdo->prepare(
                    "INSERT INTO invoices (subscription_id, learner_id, guardian_id, course_session_id, billing_month, due_date, base_amount, discount_amount, amount_due)
                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
                );

                try {
                    $ins->execute([
                        (int) $sub['subscription_id'],
                        (int) $sub['learner_id'],
                        $sub['guardian_id'] !== null ? (int) $sub['guardian_id'] : null,
                        (int) $sub['course_session_id'],
                        $billingMonth,
                        $dueDate,
                        $base,
                        $discountAmount,
                        $due,
                    ]);
                    $created++;
                } catch (PDOException $e) {
                    // If two requests race (or the button is clicked twice), the UNIQUE key can throw.
                    // Treat duplicates as skipped so generation stays idempotent.
                    if ((string) $e->getCode() === '23000') {
                        $skipped++;
                        continue;
                    }
                    throw $e;
                }
            }

            $pdo->commit();
            return ['billing_month' => $billingMonth, 'created' => $created, 'skipped' => $skipped];
        } catch (Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }
    }

    private static function discountAmount(PDO $pdo, int $subscriptionId, string $billingMonth, float $baseAmount): float
    {
        $stmt = $pdo->prepare(
            "SELECT discount_type, discount_value
             FROM subscription_discounts
             WHERE subscription_id = ?
               AND status = 'active'
               AND start_month <= ?
               AND (end_month IS NULL OR end_month >= ?)
             ORDER BY id DESC
             LIMIT 1"
        );
        $stmt->execute([$subscriptionId, $billingMonth, $billingMonth]);
        $row = $stmt->fetch();
        if (!$row) {
            return 0.0;
        }

        $value = (float) $row['discount_value'];
        if ($row['discount_type'] === 'percent') {
            return ($value / 100.0) * $baseAmount;
        }
        return $value;
    }

    public static function applyPayment(PDO $pdo, int $invoiceId, float $amount): array
    {
        $pdo->beginTransaction();
        try {
            $invStmt = $pdo->prepare('SELECT id, amount_due, amount_paid, status FROM invoices WHERE id = ? FOR UPDATE');
            $invStmt->execute([$invoiceId]);
            $inv = $invStmt->fetch();
            if (!$inv) {
                throw new RuntimeException('Invoice not found');
            }

            if ($inv['status'] === 'void') {
                throw new RuntimeException('Invoice is void');
            }

            $newPaid = (float) $inv['amount_paid'] + $amount;
            $due = (float) $inv['amount_due'];

            $newStatus = 'partial';
            if ($newPaid <= 0) {
                $newPaid = 0.0;
                $newStatus = 'unpaid';
            } elseif ($newPaid + 0.00001 >= $due) {
                $newPaid = $due;
                $newStatus = 'paid';
            }

            $up = $pdo->prepare('UPDATE invoices SET amount_paid = ?, status = ? WHERE id = ?');
            $up->execute([$newPaid, $newStatus, $invoiceId]);

            $pdo->commit();
            return ['invoice_id' => $invoiceId, 'amount_paid' => $newPaid, 'status' => $newStatus];
        } catch (Throwable $e) {
            $pdo->rollBack();
            throw $e;
        }
    }
}
