<?php

declare(strict_types=1);

namespace App\Order;

use App\Core\DatabaseInterface;

/**
 * Lecture du domaine commande pour le back-office (P4) : liste des commandes
 * recentes (order.read) + KPIs de vente du tableau de bord (stats.read).
 *
 * Separe de OrderRepository (ecriture : createPending / pay) pour garder le
 * read-side leger -- il ne depend que de DatabaseInterface, pas des repos
 * catalogue. Non `final` : seam de test (sous-classe -> double sans base).
 */
class OrderQueryRepository
{
    /**
     * Seuils de la bande SLA du KDS (RG-4 de 5.1 ; seuil cible ~10 min, Note 6).
     * Constantes plutot qu'env : un seul reglage, simple a relire et a tester ;
     * a externaliser en configuration si le besoin de variation par site apparait.
     */
    private const SLA_WARN_SECONDS = 300; // 5 min : passage vert -> ambre.
    private const SLA_LATE_SECONDS = 600; // 10 min (seuil cible) : ambre -> rouge.

    public function __construct(private readonly DatabaseInterface $db)
    {
    }

    /**
     * Commandes les plus recentes (tous statuts confondus), pour la liste admin.
     * Triees de la plus recente a la plus ancienne. $limit borne [1, 200] et
     * interpole comme ENTIER (pas de bind : LIMIT n'accepte pas de parametre lie
     * avec ATTR_EMULATE_PREPARES=false).
     *
     * @return list<array<string, mixed>>
     */
    public function recent(int $limit = 50): array
    {
        $limit = max(1, min(200, $limit));

        return $this->db->fetchAll(
            'SELECT order_number, source, service_mode, service_tag, status, total_ttc_cents, created_at, paid_at '
            . 'FROM customer_order ORDER BY created_at DESC, id DESC LIMIT ' . $limit,
        );
    }

    /**
     * Sources de commande visibles par un role (role_visible_source, dictionary 3.16).
     * Liste vide en base = vue globale (admin / manager voient tout) : on renvoie alors
     * les trois sources. Sert a filtrer la file de preparation par canal.
     *
     * @return list<string>
     */
    public function visibleSources(int $roleId): array
    {
        $rows = $this->db->fetchAll(
            'SELECT source FROM role_visible_source WHERE role_id = :r',
            ['r' => $roleId],
        );
        $sources = array_values(array_filter(array_map(
            static fn (array $r): string => (string) ($r['source'] ?? ''),
            $rows,
        )));

        return $sources === [] ? ['kiosk', 'counter', 'drive'] : $sources;
    }

    /**
     * File de preparation (KDS) : commandes au statut `paid`, triees par paid_at
     * CROISSANT (la plus ancienne d'abord, RG-T12), filtrees par les sources visibles.
     * Les sources viennent d'une allowlist (role_visible_source) et sont liees comme
     * parametres. Liste de sources vide -> file vide (pas de canal visible).
     *
     * @param list<string> $sources
     * @return list<array<string, mixed>>
     */
    public function paidQueue(array $sources): array
    {
        if ($sources === []) {
            return [];
        }

        $placeholders = [];
        $params = [];
        foreach (array_values($sources) as $i => $source) {
            $key = 's' . $i;
            $placeholders[] = ':' . $key;
            $params[$key] = $source;
        }

        return $this->db->fetchAll(
            'SELECT order_number, source, service_mode, service_tag, total_ttc_cents, paid_at '
            . 'FROM customer_order WHERE status = \'paid\' AND source IN (' . implode(', ', $placeholders) . ') '
            . 'ORDER BY paid_at ASC, id ASC',
            $params,
        );
    }

    /**
     * File de preparation enrichie pour le KDS (LIST_ORDERS_DISPLAY, mlt 5.1) :
     * memes commandes `paid` que paidQueue (meme filtre de sources, meme tri
     * paid_at croissant), mais chaque commande porte en plus :
     *  - `items`   : ses lignes order_item (label_snapshot, quantity, format),
     *                chacune avec ses `selections` (choix de slot, label_snapshot)
     *                et ses `modifiers` (ingredient + action remove/add) ;
     *  - `sla_band`: la bande SLA derivee de (now - paid_at) -- fresh / warn / late.
     *
     * RG-3 (5.1) : l'affichage s'appuie sur les SNAPSHOTS persistes ; aucune
     * re-jointure sur product/menu n'est faite. RG-4 : la couleur est calculee au
     * rendu, sans etat stocke (Note 6 du dictionnaire).
     *
     * Anti N+1 : 4 requetes au total quel que soit le nombre de commandes (la file
     * + un fetch groupe pour items / selections / modifiers via IN (...)), plutot
     * qu'un fetch par commande. L'horloge est injectable ($now) pour des bandes SLA
     * deterministes en test (meme couture ?int $now que SessionGuard / PinThrottle).
     *
     * @param list<string> $sources
     * @param int|null     $now epoch de reference pour la bande SLA ; null => time()
     * @return list<array<string, mixed>>
     */
    public function paidQueueWithDetail(array $sources, ?int $now = null): array
    {
        $now ??= time();

        if ($sources === []) {
            return [];
        }

        $placeholders = [];
        $params = [];
        foreach (array_values($sources) as $i => $source) {
            $key = 's' . $i;
            $placeholders[] = ':' . $key;
            $params[$key] = $source;
        }

        // `id` est selectionne ici (a la difference de paidQueue) : il sert de cle de
        // jointure pour le fetch groupe des lignes, sans etre expose tel quel a la vue.
        $orders = $this->db->fetchAll(
            'SELECT id, order_number, source, service_mode, service_tag, total_ttc_cents, paid_at '
            . 'FROM customer_order WHERE status = \'paid\' AND source IN (' . implode(', ', $placeholders) . ') '
            . 'ORDER BY paid_at ASC, id ASC',
            $params,
        );
        if ($orders === []) {
            return [];
        }

        $orderIds = array_map(static fn (array $o): int => (int) $o['id'], $orders);
        $items = $this->itemsForOrders($orderIds);

        $out = [];
        foreach ($orders as $order) {
            $order['items'] = $items[(int) $order['id']] ?? [];
            $order['sla_band'] = $this->slaBand((string) ($order['paid_at'] ?? ''), $now);
            unset($order['id']); // l'id technique ne sert qu'a la jointure, pas a la vue.
            $out[] = $order;
        }

        return $out;
    }

    /**
     * Bande SLA d'une commande a partir de l'ecart (now - paid_at), Note 6 / RG-4 (5.1).
     * Bandes : `fresh` si < 5 min, `warn` si 5-10 min, `late` au-dela (seuil cible
     * 10 min). Un paid_at vide ou non parsable retombe sur `fresh` (pas d'alerte sur
     * une donnee absente). Calcul pur (pas d'I/O) : la vue ne fait que mapper la bande
     * vers une classe CSS ; cf. kitchen/display.php.
     */
    public function slaBand(string $paidAt, ?int $now = null): string
    {
        $now ??= time();
        $paid = $paidAt !== '' ? strtotime($paidAt) : false;
        if ($paid === false) {
            return 'fresh';
        }

        $elapsed = $now - $paid;
        if ($elapsed >= self::SLA_LATE_SECONDS) {
            return 'late';
        }
        if ($elapsed >= self::SLA_WARN_SECONDS) {
            return 'warn';
        }

        return 'fresh';
    }

    /**
     * Charge en lot les lignes des commandes donnees (order_item + selections +
     * modifiers), regroupees par order_id puis structurees par ligne. Trois requetes
     * groupees (IN (...)) au lieu d'un fetch par commande : borne le cout a O(1)
     * aller-retours quel que soit le volume de la file. Les ids viennent de la liste
     * interne (entiers surs), interpoles comme entiers : LIMIT/IN ne lient pas avec
     * ATTR_EMULATE_PREPARES=false, et un cast (int) ferme l'injection.
     *
     * @param list<int> $orderIds
     * @return array<int, list<array<string, mixed>>> items par order_id
     */
    private function itemsForOrders(array $orderIds): array
    {
        $ids = array_values(array_unique(array_map('intval', $orderIds)));
        if ($ids === []) {
            return [];
        }
        $inOrders = implode(', ', $ids);

        $itemRows = $this->db->fetchAll(
            'SELECT id, order_id, item_type, format, label_snapshot, quantity '
            . 'FROM order_item WHERE order_id IN (' . $inOrders . ') ORDER BY id ASC',
        );
        if ($itemRows === []) {
            return [];
        }

        $itemIds = array_map(static fn (array $r): int => (int) $r['id'], $itemRows);
        $inItems = implode(', ', array_values(array_unique($itemIds)));

        $selectionsByItem = $this->groupByItem(
            $this->db->fetchAll(
                'SELECT order_item_id, label_snapshot FROM order_item_selection '
                . 'WHERE order_item_id IN (' . $inItems . ') ORDER BY id ASC',
            ),
        );
        // order_item_modifier ne stocke PAS de libelle (uniquement ingredient_id) :
        // a la difference des selections (label_snapshot present), le nom lisible vient
        // d'une jointure sur `ingredient`. Seule re-jointure necessaire (RG-3 ne
        // l'exclut que pour product/menu). Le nom d'ingredient est relativement stable ;
        // a defaut de snapshot c'est la source disponible.
        $modifiersByItem = $this->groupByItem(
            $this->db->fetchAll(
                'SELECT oim.order_item_id, oim.action, i.name AS ingredient_name '
                . 'FROM order_item_modifier oim JOIN ingredient i ON i.id = oim.ingredient_id '
                . 'WHERE oim.order_item_id IN (' . $inItems . ') ORDER BY oim.id ASC',
            ),
        );

        $itemsByOrder = [];
        foreach ($itemRows as $row) {
            $itemId = (int) $row['id'];
            $row['selections'] = $selectionsByItem[$itemId] ?? [];
            $row['modifiers'] = $modifiersByItem[$itemId] ?? [];
            $itemsByOrder[(int) $row['order_id']][] = $row;
        }

        return $itemsByOrder;
    }

    /**
     * Regroupe des lignes filles par leur order_item_id (cle de jointure commune
     * aux selections et aux modificateurs).
     *
     * @param list<array<string, mixed>> $rows
     * @return array<int, list<array<string, mixed>>>
     */
    private function groupByItem(array $rows): array
    {
        $grouped = [];
        foreach ($rows as $row) {
            $grouped[(int) ($row['order_item_id'] ?? 0)][] = $row;
        }

        return $grouped;
    }

    /**
     * KPIs de vente : CA encaisse (statuts paid + delivered), nombre de commandes
     * encaissees, panier moyen, CA et nombre du JOUR, total de commandes, et la
     * repartition par statut. Le CA exclut les commandes pending_payment (non
     * encaissees) et cancelled.
     *
     * @return array{revenue_cents:int, paid_count:int, avg_basket_cents:int, revenue_today_cents:int, paid_count_today:int, total_orders:int, by_status:array<string,int>}
     */
    public function salesKpis(): array
    {
        $t = $this->db->fetch(
            "SELECT
                COALESCE(SUM(CASE WHEN status IN ('paid','delivered') THEN total_ttc_cents ELSE 0 END), 0) AS revenue,
                COALESCE(SUM(status IN ('paid','delivered')), 0) AS paid_count,
                COALESCE(SUM(CASE WHEN status IN ('paid','delivered') AND created_at >= CURDATE() THEN total_ttc_cents ELSE 0 END), 0) AS revenue_today,
                COALESCE(SUM(status IN ('paid','delivered') AND created_at >= CURDATE()), 0) AS paid_count_today,
                COUNT(*) AS total_orders
             FROM customer_order",
        ) ?? [];

        $revenue = (int) ($t['revenue'] ?? 0);
        $paid = (int) ($t['paid_count'] ?? 0);

        $byStatus = [];
        foreach ($this->db->fetchAll('SELECT status, COUNT(*) AS n FROM customer_order GROUP BY status') as $r) {
            $byStatus[(string) ($r['status'] ?? '')] = (int) ($r['n'] ?? 0);
        }

        return [
            'revenue_cents'       => $revenue,
            'paid_count'          => $paid,
            'avg_basket_cents'    => $paid > 0 ? intdiv($revenue, $paid) : 0,
            'revenue_today_cents' => (int) ($t['revenue_today'] ?? 0),
            'paid_count_today'    => (int) ($t['paid_count_today'] ?? 0),
            'total_orders'        => (int) ($t['total_orders'] ?? 0),
            'by_status'           => $byStatus,
        ];
    }
}
