<?php

namespace App\Services\Refunds;

use App\Models\Configuration;
use App\Models\Invoice;
use App\Models\Refund;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;

class RefundOverpaymentService
{
    public function __construct(private RefundNotificationService $notifications)
    {
    }

    public function recalculate(bool $triggerEvents = false, ?string $invoiceNumber = null, ?int $actorId = null): array
    {
        if ($invoiceNumber !== null && $invoiceNumber !== '') {
            return $this->recalculateForInvoice($invoiceNumber, $triggerEvents, $actorId);
        }

        $created = [];
        $updated = [];
        $schoolYear = (string) (Configuration::getConfig('school_year') ?? '');
        $semester = (string) (Configuration::getConfig('semester') ?? '');

        $paidRows = $this->sumPaymentsByParent($schoolYear);
        $invRows = $this->sumInvoicesByParent($schoolYear);
        $refRows = $this->sumRefundsByParent($schoolYear);

        $paidMap = $this->indexByParent($paidRows, 'total_paid');
        $invMap = $this->indexByParent($invRows, 'total_invoiced');
        $refMap = $this->indexByParent($refRows, 'total_refunded');

        $parentIds = array_unique(array_merge(array_keys($paidMap), array_keys($invMap)));

        foreach ($parentIds as $pid) {
            $paid = (float) ($paidMap[$pid] ?? 0);
            $invd = (float) ($invMap[$pid] ?? 0);
            $rfnd = (float) ($refMap[$pid] ?? 0);
            $over = round($paid - $invd - $rfnd, 2);
            if ($over > 0.0001) {
                $existing = Refund::query()
                    ->where('parent_id', $pid)
                    ->where('school_year', $schoolYear)
                    ->where('request', 'overpayment')
                    ->orderByDesc('id')
                    ->first();

                if ($existing && in_array($existing->status, [Refund::STATUS_PENDING, Refund::STATUS_PARTIAL], true)) {
                    $existing->update([
                        'refund_amount' => $over,
                        'updated_at' => utc_now(),
                        'updated_by' => $actorId,
                    ]);
                    $updated[] = (int) $existing->id;
                }
            }
        }

        try {
            $createdUpdated = $this->recalculatePerInvoice($schoolYear, $semester, $triggerEvents, $actorId);
            $created = array_merge($created, $createdUpdated['created_ids'] ?? []);
            $updated = array_merge($updated, $createdUpdated['updated_ids'] ?? []);
        } catch (\Throwable $e) {
            Log::error('recalculateOverpayments per-invoice failed', ['error' => $e->getMessage()]);
        }

        return ['created_ids' => $created, 'updated_ids' => $updated];
    }

    private function recalculateForInvoice(string $invoiceNumber, bool $triggerEvents, ?int $actorId): array
    {
        $invoice = Invoice::query()->where('invoice_number', $invoiceNumber)->first();
        if (!$invoice) {
            return ['ok' => false, 'message' => 'Invoice not found.'];
        }

        $iid = (int) $invoice->id;
        $pid = (int) $invoice->parent_id;
        $year = (string) $invoice->school_year;

        $paid = (float) DB::table('payments')
            ->selectRaw('COALESCE(SUM(paid_amount),0) AS tot')
            ->where('invoice_id', $iid)
            ->value('tot');

        $disc = (float) DB::table('discount_usages')
            ->selectRaw('COALESCE(SUM(discount_amount),0) AS tot')
            ->where('invoice_id', $iid)
            ->value('tot');

        $rfnd = (float) DB::table('refunds')
            ->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS tot')
            ->where('invoice_id', $iid)
            ->whereIn('status', ['Partial', 'Paid'])
            ->value('tot');

        $total = (float) ($invoice->total_amount ?? 0);
        $raw = round($total - $disc - $paid - $rfnd, 2);
        if ($raw >= -0.0001) {
            return ['ok' => true, 'message' => 'No overpayment detected.'];
        }

        $over = abs($raw);
        $openRow = Refund::query()
            ->where('invoice_id', $iid)
            ->whereIn('status', [Refund::STATUS_PENDING, Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL])
            ->orderByDesc('id')
            ->first();

        if ($openRow) {
            $openRow->update([
                'refund_amount' => $over,
                'request' => 'overpayment',
                'updated_at' => utc_now(),
                'updated_by' => $actorId,
            ]);
            return ['ok' => true, 'message' => 'Overpayment updated for invoice.'];
        }

        $existing = Refund::query()
            ->where('invoice_id', $iid)
            ->where('request', 'overpayment')
            ->orderByDesc('id')
            ->first();
        if ($existing) {
            return ['ok' => true, 'message' => 'Overpayment already resolved for invoice.'];
        }

        $refund = Refund::query()->create([
            'parent_id' => $pid,
            'school_year' => $year,
            'semester' => $invoice->semester,
            'invoice_id' => $iid,
            'refund_amount' => $over,
            'refund_paid_amount' => 0.00,
            'status' => Refund::STATUS_PENDING,
            'request' => 'overpayment',
            'reason' => 'Auto-detected per-invoice overpayment (manual)',
            'updated_by' => $actorId,
            'created_at' => utc_now(),
            'updated_at' => utc_now(),
        ]);

        if ($refund && $triggerEvents) {
            $this->notifications->notifyPending($pid, $over, url('/login'));
        }

        return ['ok' => true, 'message' => 'Overpayment detected and refund created.'];
    }

    private function recalculatePerInvoice(string $schoolYear, string $semester, bool $triggerEvents, ?int $actorId): array
    {
        $created = [];
        $updated = [];

        $invRows = Invoice::query()
            ->select('id', 'parent_id', 'total_amount', 'school_year')
            ->where('school_year', $schoolYear)
            ->get()
            ->map(fn ($row) => (array) $row)
            ->all();

        if (empty($invRows)) {
            return ['created_ids' => [], 'updated_ids' => []];
        }

        $invoiceIds = array_map(static fn ($row) => (int) $row['id'], $invRows);

        $paidByInv = $this->sumPaymentsByInvoice($invoiceIds);
        $discByInv = $this->sumDiscountsByInvoice($invoiceIds);
        $refByInv = $this->sumRefundsByInvoice($invoiceIds);

        foreach ($invRows as $inv) {
            $iid = (int) $inv['id'];
            $pid = (int) $inv['parent_id'];
            $total = (float) ($inv['total_amount'] ?? 0);
            $paid = (float) ($paidByInv[$iid] ?? 0);
            $disc = (float) ($discByInv[$iid] ?? 0);
            $rfnd = (float) ($refByInv[$iid] ?? 0);

            $rawBalance = round($total - $disc - $paid - $rfnd, 2);
            if ($rawBalance >= -0.0001) {
                continue;
            }

            $over = abs($rawBalance);

            $openRow = Refund::query()
                ->where('invoice_id', $iid)
                ->whereIn('status', [Refund::STATUS_PENDING, Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL])
                ->orderByDesc('id')
                ->first();
            if ($openRow) {
                $openRow->update([
                    'refund_amount' => $over,
                    'updated_at' => utc_now(),
                    'updated_by' => $actorId,
                ]);
                $updated[] = (int) $openRow->id;
                continue;
            }

            $parentLevelOpen = Refund::query()
                ->where('parent_id', $pid)
                ->where('school_year', $schoolYear)
                ->whereNull('invoice_id')
                ->where('request', 'overpayment')
                ->whereIn('status', [Refund::STATUS_PENDING, Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL])
                ->orderByDesc('id')
                ->first();

            if ($parentLevelOpen) {
                $parentLevelOpen->update([
                    'invoice_id' => $iid,
                    'refund_amount' => $over,
                    'updated_at' => utc_now(),
                    'updated_by' => $actorId,
                ]);
                $updated[] = (int) $parentLevelOpen->id;
                continue;
            }

            $existing = Refund::query()
                ->where('invoice_id', $iid)
                ->where('request', 'overpayment')
                ->whereIn('status', [Refund::STATUS_PENDING, Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL, Refund::STATUS_PAID])
                ->orderByDesc('id')
                ->first();

            if (!$existing || $existing->status === Refund::STATUS_PAID) {
                $refund = Refund::query()->create([
                    'parent_id' => $pid,
                    'school_year' => $schoolYear,
                    'semester' => $semester,
                    'invoice_id' => $iid,
                    'refund_amount' => $over,
                    'refund_paid_amount' => 0.00,
                    'status' => Refund::STATUS_PENDING,
                    'request' => 'overpayment',
                    'reason' => 'Auto-detected per-invoice overpayment',
                    'updated_by' => $actorId,
                    'created_at' => utc_now(),
                    'updated_at' => utc_now(),
                ]);

                if ($refund) {
                    $created[] = (int) $refund->id;
                    if ($triggerEvents) {
                        $this->notifications->notifyPending($pid, $over, url('/login'));
                    }
                }
            } elseif (in_array($existing->status, [Refund::STATUS_PENDING, Refund::STATUS_PARTIAL], true)) {
                $existing->update([
                    'refund_amount' => $over,
                    'updated_at' => utc_now(),
                    'updated_by' => $actorId,
                ]);
                $updated[] = (int) $existing->id;
            }
        }

        return ['created_ids' => $created, 'updated_ids' => $updated];
    }

    private function sumPaymentsByParent(string $schoolYear): array
    {
        $query = DB::table('payments')
            ->select('parent_id', DB::raw('COALESCE(SUM(paid_amount),0) AS total_paid'))
            ->where('school_year', $schoolYear)
            ->groupBy('parent_id');

        if (Schema::hasColumn('payments', 'status')) {
            $query->where(function ($q) {
                $q->whereNotIn('status', [
                    'void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled',
                ])->orWhereNull('status');
            });
        }
        if (Schema::hasColumn('payments', 'is_void')) {
            $query->where(function ($q) {
                $q->where('is_void', 0)->orWhereNull('is_void');
            });
        }

        return $query->get()->map(fn ($row) => (array) $row)->all();
    }

    private function sumInvoicesByParent(string $schoolYear): array
    {
        return DB::table('invoices')
            ->select('parent_id', DB::raw('COALESCE(SUM(total_amount),0) AS total_invoiced'))
            ->where('school_year', $schoolYear)
            ->groupBy('parent_id')
            ->get()
            ->map(fn ($row) => (array) $row)
            ->all();
    }

    private function sumRefundsByParent(string $schoolYear): array
    {
        return DB::table('refunds')
            ->select('parent_id', DB::raw('COALESCE(SUM(refund_paid_amount),0) AS total_refunded'))
            ->where('school_year', $schoolYear)
            ->whereIn('status', ['Partial', 'Paid'])
            ->groupBy('parent_id')
            ->get()
            ->map(fn ($row) => (array) $row)
            ->all();
    }

    private function sumPaymentsByInvoice(array $invoiceIds): array
    {
        $query = DB::table('payments')
            ->select('invoice_id', DB::raw('COALESCE(SUM(paid_amount),0) AS total_paid'))
            ->whereIn('invoice_id', $invoiceIds)
            ->groupBy('invoice_id');

        if (Schema::hasColumn('payments', 'status')) {
            $query->where(function ($q) {
                $q->whereNotIn('status', [
                    'void', 'voided', 'refunded', 'failed', 'chargeback', 'declined', 'reversed', 'canceled', 'cancelled',
                ])->orWhereNull('status');
            });
        }
        if (Schema::hasColumn('payments', 'is_void')) {
            $query->where(function ($q) {
                $q->where('is_void', 0)->orWhereNull('is_void');
            });
        }

        $rows = $query->get();
        $paidBy = [];
        foreach ($rows as $row) {
            $paidBy[(int) $row->invoice_id] = (float) ($row->total_paid ?? 0);
        }

        return $paidBy;
    }

    private function sumDiscountsByInvoice(array $invoiceIds): array
    {
        $rows = DB::table('discount_usages')
            ->select('invoice_id', DB::raw('COALESCE(SUM(discount_amount),0) AS total_disc'))
            ->whereIn('invoice_id', $invoiceIds)
            ->groupBy('invoice_id')
            ->get();

        $discBy = [];
        foreach ($rows as $row) {
            $discBy[(int) $row->invoice_id] = (float) ($row->total_disc ?? 0);
        }

        return $discBy;
    }

    private function sumRefundsByInvoice(array $invoiceIds): array
    {
        $rows = DB::table('refunds')
            ->select('invoice_id', DB::raw('COALESCE(SUM(refund_paid_amount),0) AS total_ref'))
            ->whereIn('invoice_id', $invoiceIds)
            ->whereIn('status', ['Partial', 'Paid'])
            ->groupBy('invoice_id')
            ->get();

        $refBy = [];
        foreach ($rows as $row) {
            $refBy[(int) $row->invoice_id] = (float) ($row->total_ref ?? 0);
        }

        return $refBy;
    }

    private function indexByParent(array $rows, string $field): array
    {
        $map = [];
        foreach ($rows as $row) {
            $pid = (int) ($row['parent_id'] ?? 0);
            if ($pid <= 0) {
                continue;
            }
            $map[$pid] = (float) ($row[$field] ?? 0);
        }

        return $map;
    }
}
