<?php

namespace App\Services;

use CodeIgniter\Database\BaseBuilder;
use Config\Tieasy;

/**
 * Occurrence-level analytics for the authenticated user (Phase 9 / V1).
 *
 * Date scope: all currently retained non-deleted occurrences owned by the user.
 * Analytics does not inherit the Dashboard Today filter and has no date picker.
 * Undated occurrences (`occurrence_date IS NULL`) still count.
 *
 * Unexpected occurrence statuses (anything other than pending/completed) are
 * excluded from the status total rather than treated as Pending. The current
 * schema is an ENUM of those two values; this guard is defensive.
 *
 * Category analytics uses pending occurrences only and omits categories with
 * zero pending occurrences. Soft-deleted categories are not listed; tasks that
 * were moved count under their current category.
 */
class AnalyticsService
{
    public const EMPTY_MESSAGE          = 'No task data yet.';
    public const EMPTY_CATEGORY_MESSAGE = 'No pending tasks yet.';

    public const JSON_FLAGS = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE;

    /** Doughnut colors matching app.css priority tokens (visual only, not labels). */
    private const PRIORITY_COLORS = [
        1 => '#e53935',
        2 => '#fb8c00',
        3 => '#29b6f6',
        4 => '#7cb342',
    ];

    private const STATUS_PENDING_COLOR   = '#e53935';
    private const STATUS_COMPLETED_COLOR = '#7cb342';

    /** Distinct palette for category slices; not user-controlled. */
    private const CATEGORY_COLORS = [
        '#7e57c2',
        '#26a69a',
        '#ec407a',
        '#5c6bc0',
        '#8d6e63',
        '#29b6f6',
        '#fb8c00',
        '#7cb342',
        '#ef5350',
        '#42a5f5',
    ];

    public function __construct(
        private readonly Tieasy $config = new Tieasy(),
    ) {
    }

    /**
     * @return array{
     *     status: array<string, mixed>,
     *     priority: array<string, mixed>,
     *     category: array<string, mixed>,
     *     chart: array<string, mixed>
     * }
     */
    public function build(int $userId): array
    {
        $status   = $this->statusBreakdown($userId);
        $priority = $this->priorityBreakdown($userId);
        $category = $this->categoryPendingBreakdown($userId);

        return [
            'status'   => $status,
            'priority' => $priority,
            'category' => $category,
            'chart'    => $this->chartPayload($status, $priority, $category),
        ];
    }

    /**
     * Safe JSON for an inline application/json script tag (XSS-resistant).
     */
    public function encodeForScript(array $chart): string
    {
        return json_encode($chart, self::JSON_FLAGS | JSON_THROW_ON_ERROR);
    }

    /**
     * percentage = count / total * 100. Zero total yields 0 (no division).
     */
    public static function ratioPercent(int $count, int $total): float
    {
        if ($total <= 0) {
            return 0.0;
        }

        return ($count / $total) * 100;
    }

    /**
     * Whole number when the one-decimal rounding is exact; otherwise one decimal.
     * Independently rounded slices are not required to sum to 100.0%.
     */
    public static function formatPercent(float $percent): string
    {
        $tenths = (int) round($percent * 10);
        if ($tenths % 10 === 0) {
            return (string) (int) ($tenths / 10);
        }

        return number_format($tenths / 10, 1, '.', '');
    }

    /**
     * @return array{
     *     total: int,
     *     is_empty: bool,
     *     pending: array{count: int, percent: float, percent_label: string},
     *     completed: array{count: int, percent: float, percent_label: string}
     * }
     */
    public function statusBreakdown(int $userId): array
    {
        $pending   = 0;
        $completed = 0;

        foreach ($this->statusCounts($userId) as $row) {
            $status = (string) ($row['status'] ?? '');
            $count  = (int) ($row['cnt'] ?? 0);

            if ($status === 'pending') {
                $pending = $count;
            } elseif ($status === 'completed') {
                $completed = $count;
            }
            // Unexpected statuses are ignored (not counted as pending).
        }

        $total = $pending + $completed;

        return [
            'total'     => $total,
            'is_empty'  => $total === 0,
            'pending'   => $this->slice($pending, $total),
            'completed' => $this->slice($completed, $total),
        ];
    }

    /**
     * All four Tieasy priority keys are always present, including zero counts.
     *
     * @return array{total: int, is_empty: bool, items: list<array<string, mixed>>}
     */
    public function priorityBreakdown(int $userId, ?int $knownTotal = null): array
    {
        $counts = [];
        foreach ($this->priorityCounts($userId) as $row) {
            $key = (int) ($row['priority'] ?? 0);
            if (isset($this->config->priorities[$key])) {
                $counts[$key] = (int) ($row['cnt'] ?? 0);
            }
        }

        $total = $knownTotal;
        if ($total === null) {
            $total = 0;
            foreach ($counts as $count) {
                $total += $count;
            }
        }

        $items = [];
        foreach ($this->config->priorities as $key => $label) {
            $count   = $counts[$key] ?? 0;
            $slice   = $this->slice($count, $total);
            $items[] = [
                'key'           => (int) $key,
                'label'         => $label,
                'count'         => $slice['count'],
                'percent'       => $slice['percent'],
                'percent_label' => $slice['percent_label'],
                'color'         => self::PRIORITY_COLORS[$key] ?? '#9e9e9e',
            ];
        }

        return [
            'total'    => $total,
            'is_empty' => $total === 0,
            'items'    => $items,
        ];
    }

    /**
     * Pending-only. Categories with zero pending occurrences are omitted.
     *
     * @return array{total: int, is_empty: bool, items: list<array<string, mixed>>}
     */
    public function categoryPendingBreakdown(int $userId): array
    {
        $rows  = $this->categoryPendingCounts($userId);
        $total = 0;
        foreach ($rows as $row) {
            $total += (int) ($row['cnt'] ?? 0);
        }

        $items = [];
        foreach ($rows as $index => $row) {
            $count   = (int) ($row['cnt'] ?? 0);
            $slice   = $this->slice($count, $total);
            $items[] = [
                'id'            => (int) ($row['category_id'] ?? 0),
                'name'          => (string) ($row['category_name'] ?? ''),
                'count'         => $slice['count'],
                'percent'       => $slice['percent'],
                'percent_label' => $slice['percent_label'],
                'color'         => self::CATEGORY_COLORS[$index % count(self::CATEGORY_COLORS)],
            ];
        }

        return [
            'total'    => $total,
            'is_empty' => $total === 0,
            'items'    => $items,
        ];
    }

    /**
     * Server-computed chart payload. Chart.js only renders these numbers.
     *
     * @param array<string, mixed> $status
     * @param array<string, mixed> $priority
     * @param array<string, mixed> $category
     *
     * @return array<string, mixed>
     */
    public function chartPayload(array $status, array $priority, array $category): array
    {
        $priorityLabels = [];
        $priorityCounts = [];
        $priorityColors = [];
        foreach ($priority['items'] as $item) {
            $priorityLabels[] = $item['label'];
            $priorityCounts[] = $item['count'];
            $priorityColors[] = $item['color'];
        }

        $categoryLabels = [];
        $categoryCounts = [];
        $categoryColors = [];
        foreach ($category['items'] as $item) {
            $categoryLabels[] = $item['name'];
            $categoryCounts[] = $item['count'];
            $categoryColors[] = $item['color'];
        }

        return [
            'status' => [
                'empty'  => (bool) $status['is_empty'],
                'labels' => ['Pending', 'Completed'],
                'counts' => [
                    $status['pending']['count'],
                    $status['completed']['count'],
                ],
                'colors' => [self::STATUS_PENDING_COLOR, self::STATUS_COMPLETED_COLOR],
            ],
            'priority' => [
                'empty'  => (bool) $priority['is_empty'],
                'labels' => $priorityLabels,
                'counts' => $priorityCounts,
                'colors' => $priorityColors,
            ],
            'category' => [
                'empty'  => (bool) $category['is_empty'],
                'labels' => $categoryLabels,
                'counts' => $categoryCounts,
                'colors' => $categoryColors,
            ],
        ];
    }

    /**
     * @return array{count: int, percent: float, percent_label: string}
     */
    private function slice(int $count, int $total): array
    {
        $percent = self::ratioPercent($count, $total);

        return [
            'count'         => $count,
            'percent'       => $percent,
            'percent_label' => self::formatPercent($percent),
        ];
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function statusCounts(int $userId): array
    {
        return $this->baseQuery($userId)
            ->select('o.status AS status, COUNT(*) AS cnt', false)
            ->groupBy('o.status')
            ->get()
            ->getResultArray();
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function priorityCounts(int $userId): array
    {
        return $this->baseQuery($userId)
            ->select('t.priority AS priority, COUNT(*) AS cnt', false)
            ->groupBy('t.priority')
            ->get()
            ->getResultArray();
    }

    /**
     * @return list<array<string, mixed>>
     */
    private function categoryPendingCounts(int $userId): array
    {
        return $this->baseQuery($userId)
            ->select('c.id AS category_id, c.name AS category_name, COUNT(*) AS cnt', false)
            ->where('o.status', 'pending')
            ->groupBy('c.id')
            ->groupBy('c.name')
            ->orderBy('cnt', 'DESC')
            ->orderBy('c.name', 'ASC')
            ->get()
            ->getResultArray();
    }

    /**
     * User-scoped qualifying occurrences: owned non-deleted tasks, non-deleted
     * categories the user owns, and non-deleted occurrence rows.
     */
    private function baseQuery(int $userId): BaseBuilder
    {
        return db_connect()->table('task_occurrences as o')
            ->join('tasks as t', 't.id = o.task_id')
            ->join('categories as c', 'c.id = t.category_id')
            ->where('t.user_id', $userId)
            ->where('c.user_id', $userId)
            ->where('t.deleted_at', null)
            ->where('c.deleted_at', null)
            ->where('o.is_deleted', 0);
    }
}
