<?php
declare(strict_types=1);

namespace App\Modules\Statistics;

use PDO;
use Throwable;

final class OrdersStatisticsRepository
{
    private static ?bool $hasOrdersTotalWithoutTax = null;
    private static ?bool $hasOrdersTotalNet = null;
    private static ?bool $hasOrdersTotalWithTax = null;
    private static ?bool $hasOrdersTotalGross = null;
    private static ?bool $hasOrdersIntegrationId = null;
    private static ?bool $hasOrdersStatusCode = null;
    private static ?bool $hasOrdersExternalStatusId = null;

    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * @return array<int, array{id:int,name:string}>
     */
    public function listStatusGroups(): array
    {
        try {
            $stmt = $this->pdo->query(
                'SELECT id, name
                 FROM order_status_groups
                 WHERE is_active = 1
                 ORDER BY sort_order ASC, id ASC'
            );
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows)) {
            return [];
        }

        $groups = [];
        foreach ($rows as $row) {
            $groupId = (int) ($row['id'] ?? 0);
            if ($groupId <= 0) {
                continue;
            }

            $groups[] = [
                'id' => $groupId,
                'name' => trim((string) ($row['name'] ?? '')),
            ];
        }

        return $groups;
    }

    /**
     * @return array<int, array{key:string,label:string}>
     */
    public function listChannelOptions(): array
    {
        $hasIntegrationId = $this->hasOrdersColumn('integration_id');

        try {
            if ($hasIntegrationId) {
                $rows = $this->pdo->query(
                    'SELECT DISTINCT COALESCE(o.integration_id, 0) AS integration_id
                     FROM orders o
                     WHERE LOWER(COALESCE(o.source, "")) = "shoppro"
                     ORDER BY integration_id ASC'
                )->fetchAll(PDO::FETCH_ASSOC);
            } else {
                $rows = $this->pdo->query(
                    'SELECT 0 AS integration_id
                     WHERE EXISTS (
                        SELECT 1
                        FROM orders o
                        WHERE LOWER(COALESCE(o.source, "")) = "shoppro"
                     )'
                )->fetchAll(PDO::FETCH_ASSOC);
            }
        } catch (Throwable) {
            $rows = [];
        }

        $channels = [
            ['key' => 'allegro', 'label' => 'Allegro'],
        ];

        if (!is_array($rows)) {
            return $channels;
        }

        foreach ($rows as $row) {
            $integrationId = (int) ($row['integration_id'] ?? 0);
            $channels[] = [
                'key' => 'shoppro:' . $integrationId,
                'label' => $this->shopproChannelLabel($integrationId),
            ];
        }

        return $channels;
    }

    /**
     * @param array<int, int> $groupIds
     * @return array<int, string>
     */
    public function statusCodesByGroupIds(array $groupIds): array
    {
        $groupIds = array_values(array_unique(array_filter($groupIds, static fn (int $id): bool => $id > 0)));
        if ($groupIds === []) {
            return [];
        }

        [$inSql, $params] = $this->buildIntegerInClause('gid', $groupIds);

        try {
            $stmt = $this->pdo->prepare(
                'SELECT code
                 FROM order_statuses
                 WHERE is_active = 1
                   AND group_id IN (' . $inSql . ')
                 ORDER BY sort_order ASC, id ASC'
            );
            $stmt->execute($params);
            $rows = $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows)) {
            return [];
        }

        $codes = [];
        foreach ($rows as $code) {
            $normalized = strtolower(trim((string) $code));
            if ($normalized !== '') {
                $codes[] = $normalized;
            }
        }

        return array_values(array_unique($codes));
    }

    /**
     * @param array<int, string> $channels
     * @param array<int, string> $statusCodes
     * @return array<int, array{day:string,channel_key:string,orders_count:int,total_net:float,total_gross:float}>
     */
    public function aggregateByDay(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array
    {
        $channels = array_values(array_unique(array_filter(
            array_map(static fn (string $item): string => trim($item), $channels),
            static fn (string $item): bool => $item !== ''
        )));
        $statusCodes = array_values(array_unique(array_filter(
            array_map(static fn (string $item): string => strtolower(trim($item)), $statusCodes),
            static fn (string $item): bool => $item !== ''
        )));

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

        $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
        $effectiveDateSql = $this->effectiveDateSql('o');
        $channelSql = $this->channelSql('o');
        $netAmountSql = $this->netAmountSql('o');
        $grossAmountSql = $this->grossAmountSql('o');
        $rawStatusSql = $this->rawStatusSql('o');

        [$channelInSql, $channelParams] = $this->buildStringInClause('ch', $channels);
        $params = array_merge($channelParams, [
            'date_from' => $dateFrom . ' 00:00:00',
            'date_to' => $dateTo . ' 23:59:59',
        ]);

        $statusFilterSql = '';
        if ($statusCodes !== []) {
            [$statusInSql, $statusParams] = $this->buildStringInClause('st', $statusCodes);
            $statusFilterSql = ' AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')';
            $params = array_merge($params, $statusParams);
        }

        $sql = 'SELECT
                    DATE(' . $effectiveDateSql . ') AS day,
                    ' . $channelSql . ' AS channel_key,
                    COUNT(*) AS orders_count,
                    SUM(' . $netAmountSql . ') AS total_net,
                    SUM(' . $grossAmountSql . ') AS total_gross
                FROM orders o
                LEFT JOIN allegro_order_status_mappings asm
                    ON o.source = "allegro"
                   AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
                WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
                  AND ' . $effectiveDateSql . ' IS NOT NULL
                  AND ' . $effectiveDateSql . ' >= :date_from
                  AND ' . $effectiveDateSql . ' <= :date_to
                  AND ' . $channelSql . ' IN (' . $channelInSql . ')
                  ' . $statusFilterSql . '
                GROUP BY DATE(' . $effectiveDateSql . '), ' . $channelSql . '
                ORDER BY DATE(' . $effectiveDateSql . ') ASC';

        try {
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable) {
            return [];
        }

        if (!is_array($rows)) {
            return [];
        }

        $result = [];
        foreach ($rows as $row) {
            $day = trim((string) ($row['day'] ?? ''));
            $channelKey = trim((string) ($row['channel_key'] ?? ''));
            if ($day === '' || $channelKey === '') {
                continue;
            }

            $result[] = [
                'day' => $day,
                'channel_key' => $channelKey,
                'orders_count' => (int) ($row['orders_count'] ?? 0),
                'total_net' => (float) ($row['total_net'] ?? 0),
                'total_gross' => (float) ($row['total_gross'] ?? 0),
            ];
        }

        return $result;
    }

    /**
     * @param array<int, string> $channels
     * @param array<int, string> $statusCodes
     * @return array<string, mixed>
     */
    public function diagnostics(string $dateFrom, string $dateTo, array $channels, array $statusCodes): array
    {
        $effectiveDateSql = $this->effectiveDateSql('o');
        $channelSql = $this->channelSql('o');
        $effectiveStatusSql = $this->effectiveStatusSql('o', 'asm');
        $rawStatusSql = $this->rawStatusSql('o');

        $data = [
            'columns' => [
                'integration_id' => $this->hasOrdersColumn('integration_id'),
                'total_with_tax' => $this->hasOrdersColumn('total_with_tax'),
                'total_gross' => $this->hasOrdersColumn('total_gross'),
                'total_without_tax' => $this->hasOrdersColumn('total_without_tax'),
                'total_net' => $this->hasOrdersColumn('total_net'),
                'status_code' => $this->hasOrdersColumn('status_code'),
                'external_status_id' => $this->hasOrdersColumn('external_status_id'),
            ],
            'counts' => [],
            'errors' => [],
        ];

        $data['counts']['in_date_and_source'] = $this->safeCount(
            'SELECT COUNT(*)
             FROM orders o
             WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
               AND ' . $effectiveDateSql . ' IS NOT NULL
               AND ' . $effectiveDateSql . ' >= :date_from
               AND ' . $effectiveDateSql . ' <= :date_to',
            [
                'date_from' => $dateFrom . ' 00:00:00',
                'date_to' => $dateTo . ' 23:59:59',
            ],
            $data['errors'],
            'in_date_and_source'
        );

        if ($channels !== []) {
            [$channelInSql, $channelParams] = $this->buildStringInClause('dbg_ch', $channels);
            $data['counts']['after_channel_filter'] = $this->safeCount(
                'SELECT COUNT(*)
                 FROM orders o
                 WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
                   AND ' . $effectiveDateSql . ' IS NOT NULL
                   AND ' . $effectiveDateSql . ' >= :date_from
                   AND ' . $effectiveDateSql . ' <= :date_to
                   AND ' . $channelSql . ' IN (' . $channelInSql . ')',
                array_merge(
                    [
                        'date_from' => $dateFrom . ' 00:00:00',
                        'date_to' => $dateTo . ' 23:59:59',
                    ],
                    $channelParams
                ),
                $data['errors'],
                'after_channel_filter'
            );
        } else {
            $data['counts']['after_channel_filter'] = 0;
        }

        if ($statusCodes !== []) {
            [$statusInSql, $statusParams] = $this->buildStringInClause('dbg_st', $statusCodes);
            $data['counts']['after_status_filter'] = $this->safeCount(
                'SELECT COUNT(*)
                 FROM orders o
                 LEFT JOIN allegro_order_status_mappings asm
                     ON o.source = "allegro"
                    AND LOWER(' . $rawStatusSql . ') = asm.allegro_status_code
                 WHERE LOWER(COALESCE(o.source, "")) IN ("allegro", "shoppro")
                   AND ' . $effectiveDateSql . ' IS NOT NULL
                   AND ' . $effectiveDateSql . ' >= :date_from
                   AND ' . $effectiveDateSql . ' <= :date_to
                   AND ' . $effectiveStatusSql . ' IN (' . $statusInSql . ')',
                array_merge(
                    [
                        'date_from' => $dateFrom . ' 00:00:00',
                        'date_to' => $dateTo . ' 23:59:59',
                    ],
                    $statusParams
                ),
                $data['errors'],
                'after_status_filter'
            );
        } else {
            $data['counts']['after_status_filter'] = $data['counts']['after_channel_filter'] ?? 0;
        }

        return $data;
    }

    /**
     * @param array<int, int> $values
     * @return array{0:string,1:array<string,int>}
     */
    private function buildIntegerInClause(string $prefix, array $values): array
    {
        $placeholders = [];
        $params = [];
        foreach (array_values($values) as $index => $value) {
            $key = $prefix . '_' . $index;
            $placeholders[] = ':' . $key;
            $params[$key] = (int) $value;
        }

        return [implode(', ', $placeholders), $params];
    }

    /**
     * @param array<int, string> $values
     * @return array{0:string,1:array<string,string>}
     */
    private function buildStringInClause(string $prefix, array $values): array
    {
        $placeholders = [];
        $params = [];
        foreach (array_values($values) as $index => $value) {
            $key = $prefix . '_' . $index;
            $placeholders[] = ':' . $key;
            $params[$key] = (string) $value;
        }

        return [implode(', ', $placeholders), $params];
    }

    /**
     * @param array<string, mixed> $params
     * @param array<int, string> $errors
     */
    private function safeCount(string $sql, array $params, array &$errors, string $label): int
    {
        try {
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            return (int) $stmt->fetchColumn();
        } catch (Throwable $exception) {
            $errors[] = $label . ': ' . $exception->getMessage();
            return -1;
        }
    }

    private function effectiveStatusSql(string $orderAlias, string $mappingAlias): string
    {
        $rawStatusSql = $this->rawStatusSql($orderAlias);

        return 'LOWER(
            CASE
                WHEN ' . $orderAlias . '.source = "allegro"
                 AND ' . $mappingAlias . '.orderpro_status_code IS NOT NULL
                 AND ' . $mappingAlias . '.orderpro_status_code <> ""
                THEN ' . $mappingAlias . '.orderpro_status_code
                ELSE ' . $rawStatusSql . '
            END
        )';
    }

    private function effectiveDateSql(string $orderAlias): string
    {
        return 'COALESCE(
            ' . $orderAlias . '.ordered_at,
            ' . $orderAlias . '.source_created_at,
            ' . $orderAlias . '.source_updated_at,
            ' . $orderAlias . '.fetched_at
        )';
    }

    private function channelSql(string $orderAlias): string
    {
        if ($this->hasOrdersColumn('integration_id')) {
            return '(CASE
                WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
                WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN CONCAT("shoppro:", COALESCE(CAST(' . $orderAlias . '.integration_id AS CHAR) COLLATE utf8mb4_unicode_ci, "0"))
                ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
            END) COLLATE utf8mb4_unicode_ci';
        }

        return '(CASE
            WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "allegro" THEN "allegro"
            WHEN LOWER(COALESCE(' . $orderAlias . '.source, "")) = "shoppro" THEN "shoppro:0"
            ELSE LOWER(COALESCE(' . $orderAlias . '.source, ""))
        END) COLLATE utf8mb4_unicode_ci';
    }

    private function shopproChannelLabel(int $integrationId): string
    {
        if ($integrationId <= 0) {
            return 'shopPRO (brak integracji)';
        }

        try {
            $stmt = $this->pdo->prepare(
                'SELECT name
                 FROM integrations
                 WHERE id = :id
                 LIMIT 1'
            );
            $stmt->execute(['id' => $integrationId]);
            $name = $stmt->fetchColumn();
        } catch (Throwable) {
            $name = false;
        }

        if (is_string($name) && trim($name) !== '') {
            return trim($name);
        }

        return 'shopPRO #' . $integrationId;
    }

    private function netAmountSql(string $orderAlias): string
    {
        $netColumn = null;
        if ($this->hasOrdersColumn('total_without_tax')) {
            $netColumn = $orderAlias . '.total_without_tax';
        } elseif ($this->hasOrdersColumn('total_net')) {
            $netColumn = $orderAlias . '.total_net';
        }

        $grossColumn = null;
        if ($this->hasOrdersColumn('total_with_tax')) {
            $grossColumn = $orderAlias . '.total_with_tax';
        } elseif ($this->hasOrdersColumn('total_gross')) {
            $grossColumn = $orderAlias . '.total_gross';
        }

        // Fallback: gdy netto z zrodla jest puste (shopPRO nie wysyla netto), wyliczamy z brutto/1.23.
        // TODO(STAT-NET): docelowo pobierac netto z shopPRO na poziomie zamowienia lub liczyc z order_items po faktycznym tax_rate.
        if ($netColumn !== null && $grossColumn !== null) {
            return 'CASE
                WHEN ' . $netColumn . ' IS NOT NULL AND ' . $netColumn . ' > 0 THEN ' . $netColumn . '
                WHEN ' . $grossColumn . ' IS NOT NULL AND ' . $grossColumn . ' > 0 THEN ROUND(' . $grossColumn . ' / 1.23, 2)
                ELSE 0
            END';
        }

        if ($netColumn !== null) {
            return 'COALESCE(' . $netColumn . ', 0)';
        }

        if ($grossColumn !== null) {
            return 'ROUND(COALESCE(' . $grossColumn . ', 0) / 1.23, 2)';
        }

        return '0';
    }

    private function grossAmountSql(string $orderAlias): string
    {
        if ($this->hasOrdersColumn('total_with_tax')) {
            return 'COALESCE(' . $orderAlias . '.total_with_tax, 0)';
        }

        if ($this->hasOrdersColumn('total_gross')) {
            return 'COALESCE(' . $orderAlias . '.total_gross, 0)';
        }

        if ($this->hasOrdersColumn('total_without_tax')) {
            return 'COALESCE(' . $orderAlias . '.total_without_tax, 0)';
        }

        if ($this->hasOrdersColumn('total_net')) {
            return 'COALESCE(' . $orderAlias . '.total_net, 0)';
        }

        return '0';
    }

    private function rawStatusSql(string $orderAlias): string
    {
        if ($this->hasOrdersColumn('status_code')) {
            return 'COALESCE(' . $orderAlias . '.status_code, "")';
        }

        if ($this->hasOrdersColumn('external_status_id')) {
            return 'COALESCE(' . $orderAlias . '.external_status_id, "")';
        }

        return '""';
    }

    private function hasOrdersColumn(string $column): bool
    {
        if ($column === 'integration_id' && self::$hasOrdersIntegrationId !== null) {
            return self::$hasOrdersIntegrationId;
        }
        if ($column === 'total_without_tax' && self::$hasOrdersTotalWithoutTax !== null) {
            return self::$hasOrdersTotalWithoutTax;
        }
        if ($column === 'total_net' && self::$hasOrdersTotalNet !== null) {
            return self::$hasOrdersTotalNet;
        }
        if ($column === 'total_with_tax' && self::$hasOrdersTotalWithTax !== null) {
            return self::$hasOrdersTotalWithTax;
        }
        if ($column === 'total_gross' && self::$hasOrdersTotalGross !== null) {
            return self::$hasOrdersTotalGross;
        }
        if ($column === 'status_code' && self::$hasOrdersStatusCode !== null) {
            return self::$hasOrdersStatusCode;
        }
        if ($column === 'external_status_id' && self::$hasOrdersExternalStatusId !== null) {
            return self::$hasOrdersExternalStatusId;
        }

        try {
            $stmt = $this->pdo->prepare(
                'SELECT COUNT(*)
                 FROM information_schema.COLUMNS
                 WHERE TABLE_SCHEMA = DATABASE()
                   AND TABLE_NAME = :table_name
                   AND COLUMN_NAME = :column_name'
            );
            $stmt->execute([
                'table_name' => 'orders',
                'column_name' => $column,
            ]);
            $exists = ((int) $stmt->fetchColumn()) > 0;
        } catch (Throwable) {
            $exists = false;
        }

        if ($column === 'total_without_tax') {
            self::$hasOrdersTotalWithoutTax = $exists;
        }
        if ($column === 'total_net') {
            self::$hasOrdersTotalNet = $exists;
        }
        if ($column === 'integration_id') {
            self::$hasOrdersIntegrationId = $exists;
        }
        if ($column === 'total_with_tax') {
            self::$hasOrdersTotalWithTax = $exists;
        }
        if ($column === 'total_gross') {
            self::$hasOrdersTotalGross = $exists;
        }
        if ($column === 'status_code') {
            self::$hasOrdersStatusCode = $exists;
        }
        if ($column === 'external_status_id') {
            self::$hasOrdersExternalStatusId = $exists;
        }

        return $exists;
    }
}
