<?php
declare(strict_types=1);

namespace App\Modules\Statistics;

use App\Core\Http\Request;
use App\Core\Http\Response;
use App\Core\Security\Csrf;
use App\Core\View\Template;
use App\Core\I18n\Translator;
use App\Modules\Auth\AuthService;
use DateInterval;
use DatePeriod;
use DateTimeImmutable;

final class OrdersStatisticsController
{
    public function __construct(
        private readonly Template $template,
        private readonly Translator $translator,
        private readonly AuthService $auth,
        private readonly OrdersStatisticsRepository $repository
    ) {
    }

    public function index(Request $request): Response
    {
        [$dateFrom, $dateTo] = $this->resolveDateRange($request);

        $statusGroups = $this->repository->listStatusGroups();
        $statusGroupOptions = $this->mapStatusGroupOptions($statusGroups);
        $selectedStatusGroups = $this->resolveSelectedStatusGroups($request, $statusGroupOptions);

        $channelOptions = $this->mapChannelOptions($this->repository->listChannelOptions());
        $selectedChannels = $this->resolveSelectedChannels($request, $channelOptions);

        $statusCodes = $this->repository->statusCodesByGroupIds($selectedStatusGroups);
        $aggregated = $this->repository->aggregateByDay($dateFrom, $dateTo, $selectedChannels, $statusCodes);
        $debugEnabled = (string) $request->input('debug', '') === '1';
        $diagnostics = $debugEnabled
            ? $this->repository->diagnostics($dateFrom, $dateTo, $selectedChannels, $statusCodes)
            : [];

        $table = $this->buildTable($dateFrom, $dateTo, $selectedChannels, $aggregated);

        $html = $this->template->render('statistics/orders', [
            'title' => $this->translator->get('statistics.orders.title'),
            'activeMenu' => 'statistics',
            'activeStatistics' => 'orders',
            'user' => $this->auth->user(),
            'csrfToken' => Csrf::token(),
            'filters' => [
                'date_from' => $dateFrom,
                'date_to' => $dateTo,
                'selected_channels' => $selectedChannels,
                'selected_status_groups' => $selectedStatusGroups,
            ],
            'channelOptions' => $channelOptions,
            'statusGroupOptions' => $statusGroupOptions,
            'table' => $table,
            'debugEnabled' => $debugEnabled,
            'diagnostics' => $diagnostics,
            'debugMeta' => [
                'selected_channels' => $selectedChannels,
                'selected_status_groups' => $selectedStatusGroups,
                'status_codes' => $statusCodes,
            ],
        ], 'layouts/app');

        return Response::html($html);
    }

    public function summary(Request $request): Response
    {
        [$dateFrom, $dateTo] = $this->resolveSummaryDateRange($request);

        $statusGroups = $this->repository->listStatusGroups();
        $statusGroupOptions = $this->mapStatusGroupOptions($statusGroups);
        $selectedStatusGroups = $this->resolveSelectedStatusGroups($request, $statusGroupOptions);

        $channelOptions = $this->mapChannelOptions($this->repository->listChannelOptions());
        $selectedChannels = $this->resolveSelectedChannels($request, $channelOptions);

        $statusCodes = $this->repository->statusCodesByGroupIds($selectedStatusGroups);
        $aggregated = $this->repository->aggregateByMonth($dateFrom, $dateTo, $selectedChannels, $statusCodes);
        $summary = $this->buildSummary($dateFrom, $dateTo, $selectedChannels, $channelOptions, $aggregated);

        $html = $this->template->render('statistics/summary', [
            'title' => $this->translator->get('statistics.summary.title'),
            'activeMenu' => 'statistics',
            'activeStatistics' => 'summary',
            'user' => $this->auth->user(),
            'csrfToken' => Csrf::token(),
            'filters' => [
                'date_from' => $dateFrom,
                'date_to' => $dateTo,
                'selected_channels' => $selectedChannels,
                'selected_status_groups' => $selectedStatusGroups,
            ],
            'channelOptions' => $channelOptions,
            'statusGroupOptions' => $statusGroupOptions,
            'summary' => $summary,
        ], 'layouts/app');

        return Response::html($html);
    }

    /**
     * @return array{0:string,1:string}
     */
    private function resolveDateRange(Request $request): array
    {
        $now = new DateTimeImmutable('now');
        $defaultFrom = $now->modify('first day of this month')->format('Y-m-d');
        $defaultTo = $now->modify('last day of this month')->format('Y-m-d');

        $dateFrom = trim((string) $request->input('date_from', $defaultFrom));
        $dateTo = trim((string) $request->input('date_to', $defaultTo));

        if (!$this->isValidDate($dateFrom)) {
            $dateFrom = $defaultFrom;
        }
        if (!$this->isValidDate($dateTo)) {
            $dateTo = $defaultTo;
        }
        if ($dateFrom > $dateTo) {
            [$dateFrom, $dateTo] = [$dateTo, $dateFrom];
        }

        return [$dateFrom, $dateTo];
    }

    /**
     * @return array{0:string,1:string}
     */
    private function resolveSummaryDateRange(Request $request): array
    {
        $now = new DateTimeImmutable('now');
        $defaultFrom = '2026-04-01';
        $defaultTo = $now->format('Y-m-d');

        $dateFrom = trim((string) $request->input('date_from', $defaultFrom));
        $dateTo = trim((string) $request->input('date_to', $defaultTo));

        if (!$this->isValidDate($dateFrom)) {
            $dateFrom = $defaultFrom;
        }
        if (!$this->isValidDate($dateTo)) {
            $dateTo = $defaultTo;
        }
        if ($dateFrom > $dateTo) {
            [$dateFrom, $dateTo] = [$dateTo, $dateFrom];
        }

        return [$dateFrom, $dateTo];
    }

    private function isValidDate(string $date): bool
    {
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) !== 1) {
            return false;
        }

        return DateTimeImmutable::createFromFormat('Y-m-d', $date) !== false;
    }

    /**
     * @param array<int, array{id:int,name:string}> $groups
     * @return array<int, array{id:int,name:string}>
     */
    private function mapStatusGroupOptions(array $groups): array
    {
        $options = [];
        foreach ($groups as $group) {
            $groupId = (int) ($group['id'] ?? 0);
            if ($groupId <= 0) {
                continue;
            }

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

        return $options;
    }

    /**
     * @param array<int, array{id:int,name:string}> $statusGroupOptions
     * @return array<int, int>
     */
    private function resolveSelectedStatusGroups(Request $request, array $statusGroupOptions): array
    {
        $allowed = [];
        foreach ($statusGroupOptions as $option) {
            $allowed[] = (int) $option['id'];
        }

        $allInput = $request->all();
        $hasStatusGroupsParam = array_key_exists('status_groups', $allInput);
        $selected = $this->toIntegerList($request->input('status_groups', []));
        if (!$hasStatusGroupsParam) {
            $selected = $this->defaultStatusGroupIds($statusGroupOptions);
        }

        $selected = array_values(array_intersect($selected, $allowed));
        if ($selected !== []) {
            return $selected;
        }

        return $hasStatusGroupsParam ? [] : $this->defaultStatusGroupIds($statusGroupOptions);
    }

    /**
     * @param array<int, array{id:int,name:string}> $statusGroupOptions
     * @return array<int, int>
     */
    private function defaultStatusGroupIds(array $statusGroupOptions): array
    {
        $selected = [];
        foreach ($statusGroupOptions as $group) {
            $name = trim((string) ($group['name'] ?? ''));
            if ($this->isCancelledGroup($name)) {
                continue;
            }
            $selected[] = (int) ($group['id'] ?? 0);
        }

        return array_values(array_filter($selected, static fn (int $id): bool => $id > 0));
    }

    private function isCancelledGroup(string $name): bool
    {
        $normalized = strtr(mb_strtolower(trim($name)), [
            'ą' => 'a',
            'ć' => 'c',
            'ę' => 'e',
            'ł' => 'l',
            'ń' => 'n',
            'ó' => 'o',
            'ś' => 's',
            'ż' => 'z',
            'ź' => 'z',
        ]);

        return in_array($normalized, ['anulowane', 'anulowany', 'cancelled', 'canceled'], true);
    }

    /**
     * @param array<int, array{key:string,label:string}> $channels
     * @return array<int, array{key:string,label:string}>
     */
    private function mapChannelOptions(array $channels): array
    {
        $options = [];
        foreach ($channels as $channel) {
            $key = trim((string) ($channel['key'] ?? ''));
            if ($key === '') {
                continue;
            }
            $options[] = [
                'key' => $key,
                'label' => trim((string) ($channel['label'] ?? $key)),
            ];
        }

        return $options;
    }

    /**
     * @param array<int, array{key:string,label:string}> $channelOptions
     * @return array<int, string>
     */
    private function resolveSelectedChannels(Request $request, array $channelOptions): array
    {
        $allowed = [];
        foreach ($channelOptions as $option) {
            $allowed[] = (string) $option['key'];
        }

        $allInput = $request->all();
        $hasChannelsParam = array_key_exists('channels', $allInput);
        $selected = $this->toStringList($request->input('channels', []));
        if (!$hasChannelsParam) {
            return $allowed;
        }

        return array_values(array_intersect($selected, $allowed));
    }

    /**
     * @param array<int, string> $selectedChannels
     * @param array<int, array{day:string,channel_key:string,orders_count:int,total_net:float,total_gross:float}> $aggregated
     * @return array{rows:array<int,array<string,mixed>>,totals:array<string,mixed>,hasData:bool}
     */
    private function buildTable(string $dateFrom, string $dateTo, array $selectedChannels, array $aggregated): array
    {
        $rows = [];
        foreach ($this->dateRange($dateFrom, $dateTo) as $day) {
            $rows[$day] = [
                'day' => $day,
                'channels' => $this->emptyChannelsRow($selectedChannels),
                'day_total_orders' => 0,
                'day_total_net' => 0.0,
                'day_total_gross' => 0.0,
            ];
        }

        foreach ($aggregated as $item) {
            $day = (string) ($item['day'] ?? '');
            $channelKey = (string) ($item['channel_key'] ?? '');
            if ($day === '' || $channelKey === '' || !isset($rows[$day]['channels'][$channelKey])) {
                continue;
            }

            $ordersCount = (int) ($item['orders_count'] ?? 0);
            $totalNet = (float) ($item['total_net'] ?? 0);
            $totalGross = (float) ($item['total_gross'] ?? 0);

            $rows[$day]['channels'][$channelKey] = [
                'orders_count' => $ordersCount,
                'total_net' => $totalNet,
                'total_gross' => $totalGross,
            ];
            $rows[$day]['day_total_orders'] += $ordersCount;
            $rows[$day]['day_total_net'] += $totalNet;
            $rows[$day]['day_total_gross'] += $totalGross;
        }

        $totals = [
            'channels' => $this->emptyChannelsRow($selectedChannels),
            'orders_count' => 0,
            'total_net' => 0.0,
            'total_gross' => 0.0,
        ];

        $hasData = false;
        foreach ($rows as $row) {
            foreach ($selectedChannels as $channelKey) {
                $channelStats = $row['channels'][$channelKey];
                $totals['channels'][$channelKey]['orders_count'] += (int) ($channelStats['orders_count'] ?? 0);
                $totals['channels'][$channelKey]['total_net'] += (float) ($channelStats['total_net'] ?? 0);
                $totals['channels'][$channelKey]['total_gross'] += (float) ($channelStats['total_gross'] ?? 0);
            }
            $totals['orders_count'] += (int) ($row['day_total_orders'] ?? 0);
            $totals['total_net'] += (float) ($row['day_total_net'] ?? 0);
            $totals['total_gross'] += (float) ($row['day_total_gross'] ?? 0);
            if ((int) ($row['day_total_orders'] ?? 0) > 0) {
                $hasData = true;
            }
        }

        return [
            'rows' => array_values($rows),
            'totals' => $totals,
            'hasData' => $hasData,
        ];
    }

    /**
     * @param array<int, string> $selectedChannels
     * @param array<int, array{key:string,label:string}> $channelOptions
     * @param array<int, array{month:string,channel_key:string,orders_count:int,total_gross:float}> $aggregated
     * @return array<string, mixed>
     */
    private function buildSummary(
        string $dateFrom,
        string $dateTo,
        array $selectedChannels,
        array $channelOptions,
        array $aggregated
    ): array {
        $channelLabels = $this->channelLabels($channelOptions);
        $rows = $this->emptySummaryRows($dateFrom, $dateTo, $selectedChannels);

        foreach ($aggregated as $item) {
            $month = (string) ($item['month'] ?? '');
            $channelKey = (string) ($item['channel_key'] ?? '');
            if ($month === '' || $channelKey === '' || !isset($rows[$month]['channels'][$channelKey])) {
                continue;
            }

            $ordersCount = (int) ($item['orders_count'] ?? 0);
            $totalGross = (float) ($item['total_gross'] ?? 0);
            $rows[$month]['channels'][$channelKey] = [
                'orders_count' => $ordersCount,
                'total_gross' => $totalGross,
            ];
            $rows[$month]['total_orders_count'] += $ordersCount;
            $rows[$month]['total_gross'] += $totalGross;
        }

        return $this->summaryPayload($rows, $selectedChannels, $channelLabels);
    }

    /**
     * @param array<int, array{key:string,label:string}> $channelOptions
     * @return array<string, string>
     */
    private function channelLabels(array $channelOptions): array
    {
        $labels = [];
        foreach ($channelOptions as $option) {
            $key = (string) ($option['key'] ?? '');
            if ($key !== '') {
                $labels[$key] = (string) ($option['label'] ?? $key);
            }
        }

        return $labels;
    }

    /**
     * @param array<int, string> $selectedChannels
     * @return array<string, array{month:string,channels:array<string,array{orders_count:int,total_gross:float}>,total_orders_count:int,total_gross:float}>
     */
    private function emptySummaryRows(string $dateFrom, string $dateTo, array $selectedChannels): array
    {
        $rows = [];
        foreach ($this->monthRange($dateFrom, $dateTo) as $month) {
            $rows[$month] = [
                'month' => $month,
                'channels' => $this->emptySummaryChannels($selectedChannels),
                'total_orders_count' => 0,
                'total_gross' => 0.0,
            ];
        }

        return $rows;
    }

    /**
     * @param array<int, string> $selectedChannels
     * @return array<string, array{orders_count:int,total_gross:float}>
     */
    private function emptySummaryChannels(array $selectedChannels): array
    {
        $channels = [];
        foreach ($selectedChannels as $channelKey) {
            $channels[$channelKey] = [
                'orders_count' => 0,
                'total_gross' => 0.0,
            ];
        }

        return $channels;
    }

    /**
     * @param array<string, array{month:string,channels:array<string,array{orders_count:int,total_gross:float}>,total_orders_count:int,total_gross:float}> $rows
     * @param array<int, string> $selectedChannels
     * @param array<string, string> $channelLabels
     * @return array<string, mixed>
     */
    private function summaryPayload(array $rows, array $selectedChannels, array $channelLabels): array
    {
        $months = array_keys($rows);
        $labels = array_map(fn (string $month): string => $this->displayMonth($month), $months);
        $countSeries = $this->summarySeries($rows, $selectedChannels, $channelLabels, 'orders_count');
        $valueSeries = $this->summarySeries($rows, $selectedChannels, $channelLabels, 'total_gross');

        $countSeries[] = $this->totalSeries($rows, 'Razem', 'total_orders_count');
        $valueSeries[] = $this->totalSeries($rows, 'Razem', 'total_gross');

        return [
            'months' => $months,
            'rows' => array_values($rows),
            'hasData' => $this->summaryHasData($rows),
            'countChart' => [
                'labels' => $labels,
                'series' => $countSeries,
                'valueType' => 'number',
            ],
            'valueChart' => [
                'labels' => $labels,
                'series' => $valueSeries,
                'valueType' => 'money',
            ],
        ];
    }

    private function displayMonth(string $month): string
    {
        $date = DateTimeImmutable::createFromFormat('Y-m-d', $month . '-01');
        if (!$date instanceof DateTimeImmutable) {
            return $month;
        }

        return $date->format('m-Y');
    }

    /**
     * @param array<string, array{channels:array<string,array{orders_count:int,total_gross:float}>}> $rows
     * @param array<int, string> $selectedChannels
     * @param array<string, string> $channelLabels
     * @return array<int, array{key:string,label:string,values:array<int,int|float>}>
     */
    private function summarySeries(array $rows, array $selectedChannels, array $channelLabels, string $metric): array
    {
        $series = [];
        foreach ($selectedChannels as $channelKey) {
            $values = [];
            foreach ($rows as $row) {
                $channelStats = $row['channels'][$channelKey] ?? [];
                $values[] = $metric === 'orders_count'
                    ? (int) ($channelStats[$metric] ?? 0)
                    : (float) ($channelStats[$metric] ?? 0);
            }

            $series[] = [
                'key' => $channelKey,
                'label' => $channelLabels[$channelKey] ?? $channelKey,
                'values' => $values,
            ];
        }

        return $series;
    }

    /**
     * @param array<string, array<string, mixed>> $rows
     * @return array{key:string,label:string,values:array<int,int|float>}
     */
    private function totalSeries(array $rows, string $label, string $metric): array
    {
        $values = [];
        foreach ($rows as $row) {
            $values[] = $metric === 'total_orders_count'
                ? (int) ($row[$metric] ?? 0)
                : (float) ($row[$metric] ?? 0);
        }

        return [
            'key' => 'total',
            'label' => $label,
            'values' => $values,
        ];
    }

    /**
     * @param array<string, array{total_orders_count:int}> $rows
     */
    private function summaryHasData(array $rows): bool
    {
        foreach ($rows as $row) {
            if ((int) ($row['total_orders_count'] ?? 0) > 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<int, string> $channelKeys
     * @return array<string, array{orders_count:int,total_net:float,total_gross:float}>
     */
    private function emptyChannelsRow(array $channelKeys): array
    {
        $row = [];
        foreach ($channelKeys as $channelKey) {
            $row[$channelKey] = [
                'orders_count' => 0,
                'total_net' => 0.0,
                'total_gross' => 0.0,
            ];
        }

        return $row;
    }

    /**
     * @return array<int, string>
     */
    private function dateRange(string $dateFrom, string $dateTo): array
    {
        $start = new DateTimeImmutable($dateFrom);
        $end = (new DateTimeImmutable($dateTo))->add(new DateInterval('P1D'));
        $period = new DatePeriod($start, new DateInterval('P1D'), $end);

        $dates = [];
        foreach ($period as $date) {
            $dates[] = $date->format('Y-m-d');
        }

        return $dates;
    }

    /**
     * @return array<int, string>
     */
    private function monthRange(string $dateFrom, string $dateTo): array
    {
        $start = (new DateTimeImmutable($dateFrom))->modify('first day of this month');
        $end = (new DateTimeImmutable($dateTo))->modify('first day of next month');
        $period = new DatePeriod($start, new DateInterval('P1M'), $end);

        $months = [];
        foreach ($period as $date) {
            $months[] = $date->format('Y-m');
        }

        return $months;
    }

    /**
     * @return array<int, string>
     */
    private function toStringList(mixed $value): array
    {
        $items = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []);
        $list = [];
        foreach ($items as $item) {
            $normalized = trim((string) $item);
            if ($normalized !== '') {
                $list[] = $normalized;
            }
        }

        return array_values(array_unique($list));
    }

    /**
     * @return array<int, int>
     */
    private function toIntegerList(mixed $value): array
    {
        $items = is_array($value) ? $value : ($value !== null && $value !== '' ? [$value] : []);
        $list = [];
        foreach ($items as $item) {
            $number = (int) $item;
            if ($number > 0) {
                $list[] = $number;
            }
        }

        return array_values(array_unique($list));
    }
}
