<?php

namespace App\Controllers;

use App\Models\CorporateAccountModel;
use App\Models\FolioLineItemModel;
use App\Models\FolioModel;
use App\Models\FolioPayerAllocationModel;
use App\Models\FolioPaymentModel;
use App\Models\GuestModel;
use App\Models\LoyaltyAccountModel;
use App\Models\PaymentAttemptModel;
use Config\Lookups;
use RuntimeException;

class FolioController extends BaseController
{
    /**
     * Folio list — new page (previously only reachable one at a time,
     * from a booking or checkout). "Due Today"/"Overdue"/"Settled" in the
     * mockup aren't stored folio states (folios only have open/closed —
     * see FolioModel) — derived here from the linked booking's checkout
     * date + current balance, same honest-substitution approach as the
     * Reservations list's Due In/Due Out tabs.
     */
    public function index()
    {
        $folios = new FolioModel();

        $rows = $folios->select('folios.*, guests.full_name as guest_name, bookings.check_out_date, bookings.room_id, rooms.room_number')
            ->join('guests', 'guests.id = folios.guest_id')
            ->join('bookings', 'bookings.id = folios.booking_id', 'left')
            ->join('rooms', 'rooms.id = bookings.room_id', 'left')
            ->orderBy('folios.opened_at', 'desc')
            ->findAll();

        $today = date('Y-m-d');
        $counts = ['open' => 0, 'due_today' => 0, 'overdue' => 0, 'settled' => 0];

        foreach ($rows as $row) {
            $row->total   = $folios->totalCharges($row->id);
            $row->paid    = $folios->totalPaid($row->id);
            $row->balance = money_subtract($row->total, $row->paid);

            if ($row->status === 'closed' || (float) $row->balance <= 0) {
                $row->derived_status = 'settled';
            } elseif ($row->check_out_date !== null && $row->check_out_date < $today) {
                $row->derived_status = 'overdue';
            } elseif ($row->check_out_date === $today) {
                $row->derived_status = 'due_today';
            } else {
                $row->derived_status = 'open';
            }

            $counts[$row->derived_status]++;
        }

        $tab = $this->request->getGet('tab') ?: 'all';

        $filtered = $tab === 'all' ? $rows : array_values(array_filter($rows, static fn ($r) => $r->derived_status === $tab));

        return view('folios/index', [
            'pageTitle'    => 'Folios',
            'pageSubtitle' => 'View and manage guest folios and charges.',
            'folios'       => $filtered,
            'counts'       => $counts,
            'total'        => count($rows),
            'tab'          => $tab,
        ]);
    }

    public function show(int $id)
    {
        $folios = new FolioModel();
        $folio  = $folios->find($id);

        if ($folio === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $guest      = (new GuestModel())->find($folio->guest_id);
        $lineItems  = (new FolioLineItemModel())->forFolio($id);
        $payments   = (new FolioPaymentModel())->forFolio($id);
        $allocations = (new FolioPayerAllocationModel())->forFolio($id);

        $pendingPayment = (new PaymentAttemptModel())
            ->where('folio_id', $id)->where('purpose', 'folio_payment')->where('status', 'pending')
            ->orderBy('id', 'desc')->first();

        return view('folios/show', [
            'pageTitle'      => 'Folio #' . $id,
            'folio'          => $folio,
            'guest'          => $guest,
            'lineItems'      => $lineItems,
            'payments'       => $payments,
            'allocations'    => $allocations,
            'total'          => $folios->totalCharges($id),
            'paid'           => $folios->totalPaid($id),
            'refunded'       => $folios->totalRefunded($id),
            'balance'        => $folios->balanceDue($id),
            'allocated'      => (new FolioPayerAllocationModel())->totalAllocated($id),
            'pendingPayment' => $pendingPayment,
            'corporateAccount' => $folio->corporate_account_id ? (new CorporateAccountModel())->find($folio->corporate_account_id) : null,
            'loyaltyAccount' => (new LoyaltyAccountModel())->forGuest($folio->guest_id),
            'lookups'        => new Lookups(),
        ]);
    }

    /**
     * Converts points to a folio credit - a negative-amount line item,
     * the first place this schema uses one (FolioLineItemModel's
     * validation is 'amount' => 'required|decimal', not constrained
     * positive, so this was already structurally supported). Reduces the
     * balance exactly like any other line item, no special-casing needed
     * in totalCharges()/balanceDue().
     */
    public function redeemPoints(int $id)
    {
        $folios = new FolioModel();
        $folio  = $folios->find($id);

        if ($folio === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $account = (new LoyaltyAccountModel())->forGuest($folio->guest_id);
        $points  = (int) $this->request->getPost('points');

        if ($account === null || $points <= 0 || $points > $account->points_balance) {
            return redirect()->to('folios/' . $id)->with('error', 'Enter a valid number of points - no more than the guest currently has.');
        }

        $rate  = (float) (service('settings')->get('loyalty.redemption_value_per_point') ?: 0.10);
        $value = number_format($points * $rate, 2, '.', '');

        (new FolioLineItemModel())->insert([
            'folio_id'    => $id,
            'description' => 'Loyalty points redeemed (' . $points . ' pts)',
            'amount'      => '-' . $value,
            'category'    => 'other',
            'item_date'   => date('Y-m-d'),
            'created_by'  => $this->currentUserId(),
        ]);

        (new LoyaltyAccountModel())->creditPoints(
            $folio->guest_id,
            -$points,
            'redeem',
            'folio_redemption',
            $id,
            $points . ' points redeemed for ' . format_money($value) . ' credit on folio #' . $id,
            $this->currentUserId(),
        );

        $this->logActivity('folio', "Redeemed {$points} loyalty points for " . format_money($value) . " credit on folio #{$id}.");

        return redirect()->to('folios/' . $id)->with('success', $points . ' points redeemed.');
    }

    /**
     * Tags/untags the folio for a corporate account's monthly statement —
     * independent of split-billing payer_allocations (see FolioModel).
     */
    public function setCorporateAccount(int $id)
    {
        $folios = new FolioModel();

        if ($folios->find($id) === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $accountId = (int) $this->request->getPost('corporate_account_id');
        $folios->setCorporateAccount($id, $accountId > 0 ? $accountId : null);

        $this->logActivity('folio', $accountId > 0
            ? "Tagged folio #{$id} to corporate account #{$accountId}."
            : "Removed corporate account billing from folio #{$id}.");

        return redirect()->to('folios/' . $id)->with('success', 'Updated.');
    }

    /**
     * Generates a Hubtel Checkout link for the folio's current balance.
     * The checkout itself is settled asynchronously — see
     * PaymentWebhookController — this just creates the payment_attempts
     * row and hands staff a link to send the guest.
     */
    public function requestHubtelPayment(int $id)
    {
        $folios = new FolioModel();
        $folio  = $folios->find($id);

        if ($folio === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $balance = $folios->balanceDue($id);

        if ((float) $balance <= 0) {
            return redirect()->to('folios/' . $id)->with('error', 'This folio has no outstanding balance.');
        }

        $attempts = new PaymentAttemptModel();
        $reference = $attempts->generateClientReference('folio_payment');
        $tenantSlug = service('tenantContext')->getTenant()->slug;

        try {
            $checkout = service('hubtelService')->createCheckout(
                (float) $balance,
                "Folio #{$id} balance",
                $reference,
                site_url("webhooks/hubtel/checkout/{$tenantSlug}"),
                site_url("folios/{$id}"),
                site_url("folios/{$id}"),
            );
        } catch (RuntimeException $e) {
            return redirect()->to('folios/' . $id)->with('error', $e->getMessage());
        }

        $attempts->insert([
            'purpose'          => 'folio_payment',
            'folio_id'         => $id,
            'client_reference' => $reference,
            'checkout_id'      => $checkout['checkoutId'],
            'checkout_url'     => $checkout['checkoutUrl'],
            'amount'           => $balance,
            'status'           => 'pending',
        ]);

        $this->logActivity('folio', "Generated Hubtel payment request for " . format_money($balance) . " on folio #{$id}.");

        return redirect()->to('folios/' . $id)->with('success', 'Hubtel payment link generated — send it to the guest below.');
    }

    public function addCharge(int $id)
    {
        $folios = new FolioModel();

        if ($folios->find($id) === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $lineItems = new FolioLineItemModel();
        $data = [
            'folio_id'    => $id,
            'description' => $this->request->getPost('description'),
            'amount'      => parse_money($this->request->getPost('amount')),
            'category'    => $this->request->getPost('category') ?: 'other',
            'item_date'   => $this->request->getPost('item_date') ?: date('Y-m-d'),
            'created_by'  => $this->currentUserId(),
        ];

        if (! $lineItems->insert($data)) {
            return $this->redirectWithModelErrors($lineItems);
        }

        $this->logActivity('folio', "Added charge \"{$data['description']}\" (" . format_money($data['amount']) . ") to folio #{$id}.");

        return redirect()->to('folios/' . $id)->with('success', 'Charge added.');
    }

    public function addPayment(int $id)
    {
        $folios = new FolioModel();

        if ($folios->find($id) === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $payments = new FolioPaymentModel();
        $data = [
            'folio_id'    => $id,
            'amount'      => parse_money($this->request->getPost('amount')),
            'method'      => $this->request->getPost('method'),
            'reference'   => null,
            'recorded_by' => $this->currentUserId(),
            'paid_at'     => date('Y-m-d H:i:s'),
        ];

        if (! $payments->insert($data)) {
            return $this->redirectWithModelErrors($payments);
        }

        $this->logActivity('folio', 'Recorded ' . $data['method'] . ' payment of ' . format_money($data['amount']) . " on folio #{$id}.");

        return redirect()->to('folios/' . $id)->with('success', 'Payment recorded.');
    }

    /**
     * Real email, using the SMTP credentials configured under Settings >
     * Integrations (App\Libraries\EmailService — see its own doc comment
     * for why this is a separate, narrower capability from the SMS/
     * WhatsApp guest-messaging channels). Not faked: if the hotel hasn't
     * configured SMTP yet, this fails with a real, honest error rather
     * than silently pretending to send.
     */
    public function emailInvoice(int $id)
    {
        $folios = new FolioModel();
        $folio  = $folios->find($id);

        if ($folio === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $toEmail = trim((string) $this->request->getPost('email'));

        if ($toEmail === '' || ! filter_var($toEmail, FILTER_VALIDATE_EMAIL)) {
            return redirect()->to('folios/' . $id)->with('error', 'Enter a valid email address.');
        }

        $pdf = (new InvoiceController())->renderFolioInvoicePdf($id);
        $tenant = service('tenantContext')->getTenant();

        $result = service('emailService')->send(
            $toEmail,
            'Invoice from ' . $tenant->hotel_name . ' — Folio #' . $id,
            '<p>Please find your invoice attached.</p><p>' . esc($tenant->hotel_name) . '</p>',
            ['filename' => "invoice-folio-{$id}.pdf", 'content' => $pdf, 'mime' => 'application/pdf'],
            (int) $folio->guest_id,
        );

        if (! $result['success']) {
            return redirect()->to('folios/' . $id)->with('error', $result['error']);
        }

        $this->logActivity('folio', "Emailed invoice for folio #{$id} to {$toEmail}.");

        return redirect()->to('folios/' . $id)->with('success', 'Invoice emailed to ' . $toEmail . '.');
    }

    /**
     * A refund is just another folio_payments row, flagged is_refund=1 —
     * same ledger, same table, nothing fabricated (see
     * AddIsRefundToFolioPayments + FolioModel::totalPaid()). Capped at
     * what's actually been paid net-of-refunds so far, so this can never
     * push amount paid negative.
     */
    public function refundPayment(int $id)
    {
        $folios = new FolioModel();
        $folio  = $folios->find($id);

        if ($folio === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $available = $folios->totalPaid($id);
        $amount    = parse_money($this->request->getPost('amount'));
        $reason    = trim((string) $this->request->getPost('refund_reason'));

        if ((float) $amount > (float) $available) {
            return redirect()->to('folios/' . $id)
                ->with('error', 'Cannot refund more than has been paid (' . format_money($available) . ' available).')
                ->withInput();
        }

        if ($reason === '') {
            return redirect()->to('folios/' . $id)->with('error', 'A refund reason is required.')->withInput();
        }

        $payments = new FolioPaymentModel();
        $data = [
            'folio_id'      => $id,
            'amount'        => $amount,
            'is_refund'     => 1,
            'refund_reason' => $reason,
            'method'        => $this->request->getPost('method'),
            'reference'     => null,
            'recorded_by'   => $this->currentUserId(),
            'paid_at'       => date('Y-m-d H:i:s'),
        ];

        if (! $payments->insert($data)) {
            return $this->redirectWithModelErrors($payments);
        }

        $this->logActivity('folio', 'Refunded ' . format_money($amount) . " on folio #{$id}" . ($data['refund_reason'] ? " ({$data['refund_reason']})" : '') . '.');

        return redirect()->to('folios/' . $id)->with('success', 'Refund recorded.');
    }

    /**
     * Split billing: records how much of the folio's total each payer is
     * responsible for. Purely informational bookkeeping in Phase 1 — it
     * doesn't restrict who folio_payments can be recorded against, since
     * front desk still ultimately just needs the balance to reach zero.
     */
    public function addAllocation(int $id)
    {
        $folios = new FolioModel();

        if ($folios->find($id) === null) {
            return redirect()->to('/')->with('error', 'Folio not found.');
        }

        $allocations = new FolioPayerAllocationModel();
        $data = [
            'folio_id'           => $id,
            'folio_line_item_id' => $this->request->getPost('folio_line_item_id') ?: null,
            'payer_name'         => $this->request->getPost('payer_name'),
            'payer_guest_id'     => $this->request->getPost('payer_guest_id') ?: null,
            'amount'             => parse_money($this->request->getPost('amount')),
            'notes'              => $this->request->getPost('notes') ?: null,
        ];

        if (! $allocations->insert($data)) {
            return $this->redirectWithModelErrors($allocations);
        }

        $scope = $data['folio_line_item_id'] ? " for line item #{$data['folio_line_item_id']}" : '';
        $this->logActivity('folio', "Split " . format_money($data['amount']) . " to payer \"{$data['payer_name']}\"{$scope} on folio #{$id}.");

        return redirect()->to('folios/' . $id)->with('success', 'Payer allocation added.');
    }
}
