<?php

namespace App\Services\Billing;

use App\Models\AdditionalCharge;
use App\Models\Event;
use App\Models\EventCharges;
use App\Services\ExtraCharges\ExtraChargesChargeService;
use App\Services\ExtraCharges\ExtraChargesContextService;
use App\Services\ExtraCharges\ExtraChargesInvoiceService;
use App\Services\Invoices\InvoiceGenerationService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;

/**
 * ChargeService (plan §9–10, §13 Phase 3)
 *
 * Unified entry point for extra and event charges: create, cancel, apply,
 * mark paid/unpaid, list by family, and prevent duplicate charges.
 */
class ChargeService
{
    public function __construct(
        private ExtraChargesChargeService $extraCharges,
        private ExtraChargesInvoiceService $invoices,
        private ExtraChargesContextService $context,
        private InvoiceGenerationService $invoiceGeneration
    ) {
    }

    public function createExtraCharge(array $payload): array
    {
        $parentId = (int) ($payload['parent_id'] ?? 0);
        $schoolYear = (string) ($payload['school_year'] ?? $this->context->getSchoolYear());
        $title = trim((string) ($payload['title'] ?? ''));
        $chargeType = (string) ($payload['charge_type'] ?? 'add');
        $amountAbs = round(abs((float) ($payload['amount'] ?? 0)), 2);

        if ($parentId <= 0 || $title === '' || $amountAbs <= 0) {
            return ['ok' => false, 'message' => 'Invalid extra charge payload.'];
        }

        if ($this->findDuplicateExtraCharge($parentId, $schoolYear, $title, $amountAbs, $chargeType)) {
            Log::warning('Duplicate extra charge blocked', [
                'parent_id' => $parentId,
                'title' => $title,
                'school_year' => $schoolYear,
            ]);

            return [
                'ok' => false,
                'message' => 'A matching extra charge already exists for this parent.',
                'error' => 'duplicate_charge',
            ];
        }

        $result = $this->extraCharges->createCharge(array_merge($payload, [
            'school_year' => $schoolYear,
            'semester' => $payload['semester'] ?? $this->context->getSemester(),
        ]));

        if (!($result['ok'] ?? false)) {
            return $result;
        }

        return [
            'ok' => true,
            'charge_type' => 'extra',
            'id' => (int) ($result['id'] ?? 0),
            'parent_id' => $parentId,
            'invoice_id' => $result['invoice_id'] ?? null,
            'status' => !empty($payload['invoice_id']) ? 'applied' : 'pending',
        ];
    }

    public function cancelExtraCharge(int $chargeId): array
    {
        $charge = AdditionalCharge::query()->find($chargeId);
        if (!$charge) {
            return ['ok' => false, 'message' => 'Extra charge not found.'];
        }

        if ((string) $charge->status === 'void') {
            return ['ok' => false, 'message' => 'Charge is already void.', 'error' => 'already_canceled'];
        }

        $ok = $this->extraCharges->voidCharge($charge);

        return [
            'ok' => $ok,
            'charge_type' => 'extra',
            'id' => $chargeId,
            'status' => $ok ? 'void' : (string) $charge->status,
            'message' => $ok ? null : 'Unable to void extra charge.',
        ];
    }

    public function applyExtraCharge(int $chargeId, int $invoiceId): array
    {
        $charge = AdditionalCharge::query()->find($chargeId);
        if (!$charge) {
            return ['ok' => false, 'message' => 'Extra charge not found.'];
        }

        if ((string) $charge->status === 'void') {
            return ['ok' => false, 'message' => 'Cannot apply a void charge.'];
        }

        if ((string) $charge->status === 'applied' && (int) $charge->invoice_id === $invoiceId) {
            return [
                'ok' => true,
                'charge_type' => 'extra',
                'id' => $chargeId,
                'status' => 'applied',
                'invoice_id' => $invoiceId,
            ];
        }

        if ((string) $charge->status === 'applied') {
            return ['ok' => false, 'message' => 'Charge is already applied to another invoice.'];
        }

        $chargeType = (string) ($charge->charge_type ?? 'add');
        $amountAbs = round(abs((float) $charge->amount), 2);

        DB::beginTransaction();

        try {
            $this->invoices->applyToInvoice($invoiceId, $chargeType, $amountAbs);
            $charge->invoice_id = $invoiceId;
            $charge->status = 'applied';
            $charge->save();
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            Log::error('Apply extra charge failed', ['charge_id' => $chargeId, 'error' => $e->getMessage()]);

            return ['ok' => false, 'message' => 'Unable to apply charge to invoice.'];
        }

        return [
            'ok' => true,
            'charge_type' => 'extra',
            'id' => $chargeId,
            'status' => 'applied',
            'invoice_id' => $invoiceId,
        ];
    }

    public function createEventCharge(array $payload): array
    {
        $parentId = (int) ($payload['parent_id'] ?? 0);
        $studentId = (int) ($payload['student_id'] ?? 0);
        $schoolYear = (string) ($payload['school_year'] ?? $this->context->getSchoolYear());
        $semester = (string) ($payload['semester'] ?? $this->context->getSemester());
        $eventId = isset($payload['event_id']) ? (int) $payload['event_id'] : null;
        $eventName = trim((string) ($payload['event_name'] ?? ''));
        $actorId = (int) ($payload['created_by'] ?? $payload['updated_by'] ?? 0);

        if ($parentId <= 0 || $studentId <= 0) {
            return ['ok' => false, 'message' => 'Parent and student are required.'];
        }

        $amount = isset($payload['amount']) ? (float) $payload['amount'] : null;
        if ($eventId > 0) {
            $event = Event::getEvent($eventId, $schoolYear);
            if (!$event) {
                return ['ok' => false, 'message' => 'Event not found.'];
            }
            $eventName = (string) ($event->event_name ?? $eventName);
            $amount = $amount ?? (float) ($event->amount ?? 0);
        }

        if ($eventName === '' || $amount === null || $amount < 0) {
            return ['ok' => false, 'message' => 'Event name and amount are required.'];
        }

        if ($this->findDuplicateEventCharge($parentId, $studentId, $schoolYear, $semester, $eventId, $eventName)) {
            Log::warning('Duplicate event charge blocked', [
                'parent_id' => $parentId,
                'student_id' => $studentId,
                'event_id' => $eventId,
                'event_name' => $eventName,
            ]);

            return [
                'ok' => false,
                'message' => 'This student already has an active charge for this event.',
                'error' => 'duplicate_charge',
            ];
        }

        $charge = EventCharges::query()->create([
            'event_id' => $eventId > 0 ? $eventId : null,
            'event_name' => $eventId > 0 ? null : $eventName,
            'parent_id' => $parentId,
            'student_id' => $studentId,
            'participation' => 'yes',
            'charged' => round($amount, 2),
            'amount' => round($amount, 2),
            'school_year' => $schoolYear,
            'semester' => $semester,
            'created_by' => $actorId ?: null,
            'updated_by' => $actorId ?: null,
        ]);

        $this->invoiceGeneration->generateInvoice($parentId, $schoolYear);

        return [
            'ok' => true,
            'charge_type' => 'event',
            'id' => (int) $charge->id,
            'parent_id' => $parentId,
            'student_id' => $studentId,
            'event_id' => $eventId > 0 ? $eventId : null,
            'event_name' => $eventName,
            'amount' => round($amount, 2),
            'participation_status' => 'yes',
            'payment_status' => 'unpaid',
            'cancellation_status' => 'active',
        ];
    }

    public function cancelEventCharge(int $chargeId, bool $issueCredit = true): array
    {
        $charge = EventCharges::query()->find($chargeId);
        if (!$charge) {
            return ['ok' => false, 'message' => 'Event charge not found.'];
        }

        if ($this->isEventChargeCanceled($charge)) {
            return ['ok' => false, 'message' => 'Event charge is already canceled.', 'error' => 'already_canceled'];
        }

        $amount = round(abs((float) ($charge->charged ?? $charge->amount ?? 0)), 2);
        $eventName = $this->resolveEventName($charge);
        $parentId = (int) $charge->parent_id;
        $schoolYear = (string) $charge->school_year;

        DB::beginTransaction();

        try {
            $charge->participation = 'no';
            $charge->charged = 0;
            if (Schema::hasColumn('event_charges', 'amount')) {
                $charge->amount = 0;
            }
            $charge->save();

            $creditId = null;
            if ($issueCredit && $amount > 0 && $parentId > 0) {
                $credit = $this->createExtraCharge([
                    'parent_id' => $parentId,
                    'title' => 'Event credit: ' . $eventName,
                    'description' => 'Account credit for canceled event charge #' . $chargeId,
                    'amount' => $amount,
                    'charge_type' => 'deduct',
                    'school_year' => $schoolYear,
                    'semester' => (string) ($charge->semester ?? $this->context->getSemester()),
                    'created_by' => (int) ($charge->updated_by ?? 0),
                ]);
                if ($credit['ok'] ?? false) {
                    $creditId = (int) ($credit['id'] ?? 0);
                }
            }

            $this->invoiceGeneration->generateInvoice($parentId, $schoolYear);
            DB::commit();
        } catch (\Throwable $e) {
            DB::rollBack();
            Log::error('Cancel event charge failed', ['charge_id' => $chargeId, 'error' => $e->getMessage()]);

            return ['ok' => false, 'message' => 'Unable to cancel event charge.'];
        }

        return [
            'ok' => true,
            'charge_type' => 'event',
            'id' => $chargeId,
            'cancellation_status' => 'canceled',
            'participation_status' => 'no',
            'account_credit_charge_id' => $creditId,
        ];
    }

    public function markEventChargePaid(int $chargeId, bool $paid, ?int $paymentId = null): array
    {
        $charge = EventCharges::query()->find($chargeId);
        if (!$charge) {
            return ['ok' => false, 'message' => 'Event charge not found.'];
        }

        if ($this->isEventChargeCanceled($charge)) {
            return ['ok' => false, 'message' => 'Cannot update payment on a canceled charge.'];
        }

        if (Schema::hasColumn('event_charges', 'event_paid')) {
            $charge->event_paid = $paid ? 1 : 0;
        }
        if (Schema::hasColumn('event_charges', 'event_payment_id') && $paymentId !== null) {
            $charge->event_payment_id = $paymentId;
        }
        $charge->save();

        return [
            'ok' => true,
            'charge_type' => 'event',
            'id' => $chargeId,
            'payment_status' => $paid ? 'paid' : 'unpaid',
        ];
    }

    /**
     * @return array{extra_charges: array<int, array<string, mixed>>, event_charges: array<int, array<string, mixed>>}
     */
    public function listForParent(int $parentId, ?string $schoolYear = null): array
    {
        $schoolYear = $schoolYear ?? $this->context->getSchoolYear();

        $extraRows = AdditionalCharge::query()
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->orderByDesc('id')
            ->get();

        $eventRows = EventCharges::getChargesWithEventInfo($parentId, $schoolYear);

        return [
            'parent_id' => $parentId,
            'school_year' => $schoolYear,
            'extra_charges' => $extraRows->map(fn (AdditionalCharge $row) => $this->formatExtraCharge($row))->all(),
            'event_charges' => array_map(fn (array $row) => $this->formatEventCharge($row), $eventRows),
        ];
    }

    private function findDuplicateExtraCharge(
        int $parentId,
        string $schoolYear,
        string $title,
        float $amountAbs,
        string $chargeType
    ): bool {
        $normalizedTitle = strtolower($title);
        $signedAmount = $chargeType === 'deduct' ? -$amountAbs : $amountAbs;

        return AdditionalCharge::query()
            ->where('parent_id', $parentId)
            ->where('school_year', $schoolYear)
            ->whereIn('status', ['pending', 'applied'])
            ->whereRaw('LOWER(title) = ?', [$normalizedTitle])
            ->where('amount', $signedAmount)
            ->exists();
    }

    private function findDuplicateEventCharge(
        int $parentId,
        int $studentId,
        string $schoolYear,
        string $semester,
        ?int $eventId,
        string $eventName
    ): bool {
        $query = EventCharges::query()
            ->where('parent_id', $parentId)
            ->where('student_id', $studentId)
            ->where('school_year', $schoolYear)
            ->where('semester', $semester)
            ->where('participation', 'yes')
            ->where('charged', '>', 0);

        if ($eventId > 0) {
            $query->where('event_id', $eventId);
        } else {
            $query->where('event_name', $eventName);
        }

        return $query->exists();
    }

    private function isEventChargeCanceled(EventCharges $charge): bool
    {
        if (strtolower((string) $charge->participation) === 'no') {
            return true;
        }

        return (float) ($charge->charged ?? 0) <= 0;
    }

    private function resolveEventName(EventCharges $charge): string
    {
        if (!empty($charge->event_name)) {
            return (string) $charge->event_name;
        }

        if ($charge->event_id) {
            $event = Event::query()->find($charge->event_id);
            if ($event) {
                return (string) $event->event_name;
            }
        }

        return 'Event';
    }

    private function formatExtraCharge(AdditionalCharge $row): array
    {
        return [
            'id' => (int) $row->id,
            'charge_type' => 'extra',
            'parent_id' => (int) $row->parent_id,
            'invoice_id' => $row->invoice_id ? (int) $row->invoice_id : null,
            'school_year' => $row->school_year,
            'semester' => $row->semester,
            'charge_name' => $row->title,
            'charge_description' => $row->description,
            'amount' => round(abs((float) $row->amount), 2),
            'signed_amount' => round((float) $row->amount, 2),
            'fee_type' => $row->charge_type,
            'due_date' => $row->due_date,
            'status' => $row->status,
            'approval_status' => $row->status,
            'created_by' => $row->created_by,
            'created_at' => $row->created_at,
        ];
    }

    /**
     * @param array<string, mixed> $row
     */
    private function formatEventCharge(array $row): array
    {
        $charged = (float) ($row['charged'] ?? 0);
        $canceled = strtolower((string) ($row['participation'] ?? '')) === 'no' || $charged <= 0;
        $paid = false;
        if (isset($row['event_paid'])) {
            $paid = (bool) $row['event_paid'];
        }

        return [
            'id' => (int) ($row['id'] ?? 0),
            'charge_type' => 'event',
            'event_id' => isset($row['event_id']) ? (int) $row['event_id'] : null,
            'event_name' => $row['event_name'] ?? null,
            'parent_id' => isset($row['parent_id']) ? (int) $row['parent_id'] : null,
            'student_id' => isset($row['student_id']) ? (int) $row['student_id'] : null,
            'school_year' => $row['school_year'] ?? null,
            'semester' => $row['semester'] ?? null,
            'participation_status' => $row['participation'] ?? null,
            'charge_amount' => $charged,
            'payment_status' => $paid ? 'paid' : ($canceled ? 'canceled' : 'unpaid'),
            'cancellation_status' => $canceled ? 'canceled' : 'active',
            'event_date' => $row['updated_at'] ?? $row['created_at'] ?? null,
        ];
    }
}
