<?php

namespace App\Controllers;

use App\Libraries\BetBuilderPricing;
use App\Models\BuilderMatchModel;
use App\Models\BuilderTicketModel;
use CodeIgniter\RESTful\ResourceController;

/**
 * Bet Builder API controller.
 *
 * Lifecycle:
 *   1) GET  matches            -> public list of upcoming legs (odds + prediction hidden).
 *   2) POST quote              -> stateless combined-odds + tier price for UI preview.
 *   3) POST tickets            -> creates a pending ticket, returns ticket_id +
 *                                 amount + transactionId to feed into /payments/process.
 *   4) GET  tickets/{id}       -> fetch a ticket. Predictions are only revealed
 *                                 once the ticket has status = paid.
 */
class BetBuilder extends ResourceController
{
    protected $format = 'json';

    /**
     * GET /api/bet-builder/matches
     */
    public function matches()
    {
        $matchModel = new BuilderMatchModel();
        $rows = $matchModel->upcoming();

        $public = array_map(
            static fn(array $m) => (new BuilderMatchModel())->toPublicArray($m),
            $rows
        );

        return $this->respond([
            'success' => true,
            'data' => $public,
            'tiers' => BetBuilderPricing::allTiers(),
        ]);
    }

    /**
     * POST /api/bet-builder/quote  { match_ids: [int, int, ...] }
     *
     * Stateless: computes combined odds and tier price without persisting.
     */
    public function quote()
    {
        $data = $this->request->getJSON(true) ?? [];
        $matchIds = $this->sanitizeIds($data['match_ids'] ?? []);

        if (count($matchIds) < 2) {
            return $this->failValidationErrors('Select at least 2 matches to build a ticket.');
        }

        $matchModel = new BuilderMatchModel();
        [$legs, $missingIds] = $this->loadAndValidateLegs($matchModel, $matchIds);

        if (!empty($missingIds)) {
            return $this->fail([
                'message' => 'Some selected matches are no longer available.',
                'missing_ids' => $missingIds,
            ], 422);
        }

        $combined = $this->computeCombinedOdds($legs);
        $tier = BetBuilderPricing::tierFor($combined);

        return $this->respond([
            'success' => true,
            'data' => [
                'leg_count' => count($legs),
                'combined_odds' => round($combined, 4),
                'amount' => $tier['price'],
                'tier_label' => $tier['label'],
                'currency' => 'UGX',
            ],
        ]);
    }

    /**
     * POST /api/bet-builder/tickets { msisdn, match_ids: [...] }
     *
     * Creates a pending ticket and returns { ticket_id, amount, combined_odds,
     * transactionId }. The caller then passes those to /api/payments/process
     * along with a builder_ticket_id so the payment pipeline can fulfill it.
     */
    public function create()
    {
        $data = $this->request->getJSON(true) ?? [];
        $msisdn = trim((string) ($data['msisdn'] ?? ''));
        $matchIds = $this->sanitizeIds($data['match_ids'] ?? []);

        if ($msisdn === '') {
            return $this->failValidationErrors('msisdn is required.');
        }
        if (count($matchIds) < 2) {
            return $this->failValidationErrors('Select at least 2 matches to build a ticket.');
        }

        $matchModel = new BuilderMatchModel();
        [$legs, $missingIds] = $this->loadAndValidateLegs($matchModel, $matchIds);

        if (!empty($missingIds)) {
            return $this->fail([
                'message' => 'Some selected matches are no longer available.',
                'missing_ids' => $missingIds,
            ], 422);
        }

        $combined = $this->computeCombinedOdds($legs);
        $tier = BetBuilderPricing::tierFor($combined);
        $amount = $tier['price'];

        // Snapshot odds/teams at create time so admin edits don't rewrite
        // paid tickets later.
        $snapshot = array_map(static function (array $leg): array {
            return [
                'match_id' => (int) $leg['id'],
                'team1' => $leg['team1'] ?? '',
                'team2' => $leg['team2'] ?? '',
                'league' => $leg['league'] ?? '',
                'match_time' => $leg['match_time'] ?? null,
                'odds' => (float) $leg['odds'],
            ];
        }, $legs);

        $clientTransactionId = $this->generateTransactionId();

        $ticketModel = new BuilderTicketModel();
        $ticketId = $ticketModel->insert([
            'msisdn' => $msisdn,
            'match_ids' => array_map('intval', array_column($legs, 'id')),
            'legs_snapshot' => $snapshot,
            'combined_odds' => round($combined, 4),
            'leg_count' => count($legs),
            'amount' => $amount,
            'transaction_id' => $clientTransactionId,
            'status' => 'pending',
        ], true);

        if (!$ticketId) {
            return $this->failServerError('Failed to create ticket.');
        }

        return $this->respondCreated([
            'success' => true,
            'data' => [
                'ticket_id' => (int) $ticketId,
                'amount' => $amount,
                'currency' => 'UGX',
                'leg_count' => count($legs),
                'tier_label' => $tier['label'],
                'transactionId' => $clientTransactionId,
                'status' => 'pending',
            ],
        ]);
    }

    /**
     * GET /api/bet-builder/tickets/(:num)?msisdn=...
     *
     * Returns ticket details scoped to msisdn. Predictions are only revealed
     * when the ticket is paid.
     */
    public function ticket($id = null)
    {
        if (!$id) {
            return $this->failValidationErrors('ticket id is required.');
        }
        $msisdn = trim((string) ($this->request->getGet('msisdn') ?? ''));
        if ($msisdn === '') {
            return $this->failValidationErrors('msisdn is required.');
        }

        $ticketModel = new BuilderTicketModel();
        $ticket = $ticketModel->where(['id' => $id, 'msisdn' => $msisdn])->first();

        if (!$ticket) {
            return $this->failNotFound('Ticket not found.');
        }

        return $this->respond([
            'success' => true,
            'data' => $this->projectTicket($ticket),
        ]);
    }

    /**
     * GET /api/bet-builder/tickets?msisdn=...
     *
     * Lists all tickets for a msisdn (history).
     */
    public function tickets()
    {
        $msisdn = trim((string) ($this->request->getGet('msisdn') ?? ''));
        if ($msisdn === '') {
            return $this->failValidationErrors('msisdn is required.');
        }

        $ticketModel = new BuilderTicketModel();
        $rows = $ticketModel->where('msisdn', $msisdn)
            ->orderBy('created_at', 'DESC')
            ->findAll();

        $out = array_map(fn(array $t) => $this->projectTicket($t), $rows);

        return $this->respond([
            'success' => true,
            'data' => $out,
        ]);
    }

    // ------------------------------------------------------------------
    // Internals
    // ------------------------------------------------------------------

    /**
     * Coerce incoming match_ids into a unique list of positive ints.
     *
     * @return array<int, int>
     */
    private function sanitizeIds($raw): array
    {
        if (!is_array($raw)) {
            return [];
        }
        $ids = [];
        foreach ($raw as $v) {
            $int = (int) $v;
            if ($int > 0) {
                $ids[$int] = $int;
            }
        }
        return array_values($ids);
    }

    /**
     * Load legs from DB and verify each is still eligible (upcoming, not
     * archived, kickoff in future). Returns [legs, missingIds].
     *
     * @param array<int, int> $matchIds
     * @return array{0: array<int, array<string, mixed>>, 1: array<int, int>}
     */
    private function loadAndValidateLegs(BuilderMatchModel $matchModel, array $matchIds): array
    {
        if (empty($matchIds)) {
            return [[], []];
        }

        $rows = $matchModel->whereIn('id', $matchIds)
            ->where('is_archived', 0)
            ->where('status', 'upcoming')
            ->findAll();

        $now = date('Y-m-d H:i:s');
        $valid = [];
        $foundIds = [];
        foreach ($rows as $row) {
            $kickoff = $row['match_time'] ?? null;
            if ($kickoff !== null && $kickoff !== '' && $kickoff <= $now) {
                continue; // kickoff already passed
            }
            $valid[] = $row;
            $foundIds[] = (int) $row['id'];
        }

        $missing = array_values(array_diff($matchIds, $foundIds));
        return [$valid, $missing];
    }

    private function computeCombinedOdds(array $legs): float
    {
        $combined = 1.0;
        foreach ($legs as $leg) {
            $odds = (float) ($leg['odds'] ?? 1.0);
            if ($odds <= 0) {
                $odds = 1.0;
            }
            $combined *= $odds;
        }
        return $combined;
    }

    /**
     * Project a ticket row for API output, revealing predictions only when paid.
     */
    private function projectTicket(array $ticket): array
    {
        $status = $ticket['status'] ?? 'pending';
        $rawLegs = $ticket['legs_snapshot'] ?? [];
        if (is_string($rawLegs)) {
            $decoded = json_decode($rawLegs, true);
            $legs = (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) ? $decoded : [];
        } elseif (is_array($rawLegs)) {
            $legs = $rawLegs;
        } else {
            $legs = [];
        }

        if ($status === 'paid' || $status === 'settled') {
            $matchIds = array_map(
                static fn($leg) => (int) ($leg['match_id'] ?? 0),
                $legs
            );
            $matchIds = array_filter($matchIds, static fn($id) => $id > 0);

            $predictions = [];
            if (!empty($matchIds)) {
                $matchModel = new BuilderMatchModel();
                $matches = $matchModel->whereIn('id', $matchIds)->findAll();
                foreach ($matches as $m) {
                    $predictions[(int) $m['id']] = [
                        'our_prediction' => $m['our_prediction'] ?? null,
                        'result_outcome' => $m['result_outcome'] ?? 'pending',
                        'status' => $m['status'] ?? 'upcoming',
                    ];
                }
            }

            $legs = array_map(static function (array $leg) use ($predictions) {
                $mid = (int) ($leg['match_id'] ?? 0);
                $info = $predictions[$mid] ?? [];
                $leg['our_prediction'] = $info['our_prediction'] ?? null;
                $leg['result_outcome'] = $info['result_outcome'] ?? 'pending';
                $leg['match_status'] = $info['status'] ?? 'upcoming';
                return $leg;
            }, $legs);
        } else {
            // Pending/void: do not reveal predictions or odds.
            $legs = array_map(static function (array $leg) {
                $leg['our_prediction'] = null;
                unset($leg['odds']);
                return $leg;
            }, $legs);
        }

        $isPaid = $status === 'paid' || $status === 'settled';

        return [
            'id' => (int) $ticket['id'],
            'msisdn' => $ticket['msisdn'] ?? '',
            'leg_count' => (int) ($ticket['leg_count'] ?? count($legs)),
            'combined_odds' => $isPaid
                ? (float) ($ticket['combined_odds'] ?? 1.0)
                : null,
            'amount' => (int) ($ticket['amount'] ?? 0),
            'currency' => 'UGX',
            'status' => $status,
            'transaction_id' => $ticket['transaction_id'] ?? null,
            'legs' => $legs,
            'created_at' => $ticket['created_at'] ?? null,
            'updated_at' => $ticket['updated_at'] ?? null,
        ];
    }

    /**
     * Generate a client-side transaction id used to bridge this ticket with
     * the /api/payments/process call.
     */
    private function generateTransactionId(): string
    {
        // 16 hex chars is plenty for correlating a single payment attempt.
        return 'BB' . strtoupper(bin2hex(random_bytes(8)));
    }
}
