<?php

declare(strict_types=1);

namespace App\Services;

use App\Enums\AssetStatus;
use App\Enums\EmployeeStatus;
use App\Enums\WarrantyStatus;
use App\Models\Asset;
use App\Models\AssetAssignment;
use App\Models\AssetWarranty;
use App\Models\AuditLog;
use App\Models\Employee;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;

/**
 * Everything the operational dashboard shows, assembled from the modules
 * that already exist: Assets, Assignment/Return, Warranty and Employees.
 *
 * The whole snapshot — KPIs, charts, operational widgets — is one cache
 * entry with a short, fixed TTL rather than the write-triggered invalidation
 * sketched in architecture §16. Busting the cache on every asset/assignment/
 * warranty write would mean touching AssetService, AssignmentService and
 * WarrantyService, which is exactly the "do not change previous modules'
 * business logic" boundary this phase was given. A five-minute-old count on
 * a screen that is glanced at, not transacted against, is an acceptable
 * trade for leaving those services alone.
 */
class DashboardService
{
    private const TTL_MINUTES = 5;

    private const CACHE_KEY = 'ams.dashboard.snapshot';

    /**
     * How many months of purchase history the procurement chart covers.
     */
    private const PROCUREMENT_MONTHS = 12;

    /**
     * How many slices a category/team/location chart shows before the
     * remainder is folded into a single "Other" bar — enough to be useful,
     * not so many the chart becomes a legend nobody reads.
     */
    private const CHART_TOP_N = 8;

    /**
     * How many rows each operational widget lists.
     */
    private const WIDGET_LIMIT = 8;

    /**
     * Bootstrap theme name -> hex, matching resources/scss/_tokens.scss so a
     * chart slice and a status badge for the same value are always the same
     * colour.
     *
     * @var array<string, string>
     */
    private const HEX = [
        'primary' => '#0d9488',
        'secondary' => '#6b7280',
        'success' => '#16a34a',
        'info' => '#2563eb',
        'warning' => '#d97706',
        'danger' => '#dc2626',
        'light' => '#f9fafb',
        'dark' => '#111827',
    ];

    /**
     * @return array<string, mixed>
     */
    public function snapshot(): array
    {
        return Cache::remember(self::CACHE_KEY, now()->addMinutes(self::TTL_MINUTES), fn (): array => [
            'kpis' => $this->kpis(),
            'charts' => [
                'assetsByCategory' => $this->assetsByCategory(),
                'assetsByTeam' => $this->assetsByTeam(),
                'assetsByLocation' => $this->assetsByLocation(),
                'assetStatusDistribution' => $this->assetStatusDistribution(),
                'warrantyStatus' => $this->warrantyStatus(),
                'monthlyProcurement' => $this->monthlyProcurement(),
            ],
            'widgets' => [
                'recentlyAssigned' => $this->recentlyAssigned(),
                'recentlyReturned' => $this->recentlyReturned(),
                'recentActivity' => $this->recentActivity(),
                'upcomingWarrantyExpiry' => $this->upcomingWarrantyExpiry(),
                'awaitingReturn' => $this->awaitingReturn(),
                'topHolders' => $this->topHolders(),
            ],
            'generatedAt' => now(),
        ]);
    }

    /**
     * @return array<string, array{label: string, value: int, icon: string, color: string, href: string}>
     */
    private function kpis(): array
    {
        $assetCounts = Asset::query()
            ->selectRaw('COUNT(*) as total')
            ->selectRaw("SUM(status = 'assigned') as assigned")
            ->selectRaw("SUM(status = 'in_stock' AND is_active = 1) as available")
            ->selectRaw("SUM(status = 'under_repair') as under_repair")
            ->selectRaw("SUM(status = 'lost') as lost")
            ->selectRaw("SUM(status = 'retired') as retired")
            ->first();

        $employeeCounts = Employee::query()
            ->selectRaw('COUNT(*) as total')
            ->selectRaw("SUM(status = 'active') as active")
            ->first();

        $warrantyExpiring = AssetWarranty::query()->primary()->expiringWithin(30)->count();
        $warrantyExpired = AssetWarranty::query()->primary()->expired()->count();

        return [
            'total_assets' => [
                'label' => 'Total Assets', 'value' => (int) ($assetCounts->total ?? 0),
                'icon' => 'boxes', 'color' => 'primary', 'href' => route('assets.index'),
            ],
            'assigned_assets' => [
                'label' => 'Assigned', 'value' => (int) ($assetCounts->assigned ?? 0),
                'icon' => 'person-check', 'color' => 'primary', 'href' => route('assets.index', ['status' => AssetStatus::Assigned->value]),
            ],
            'available_assets' => [
                'label' => 'Available', 'value' => (int) ($assetCounts->available ?? 0),
                'icon' => 'box-seam', 'color' => 'success', 'href' => route('assets.index', ['status' => AssetStatus::InStock->value]),
            ],
            'under_repair' => [
                'label' => 'Under Repair', 'value' => (int) ($assetCounts->under_repair ?? 0),
                'icon' => 'tools', 'color' => 'warning', 'href' => route('assets.index', ['status' => AssetStatus::UnderRepair->value]),
            ],
            'lost' => [
                'label' => 'Lost', 'value' => (int) ($assetCounts->lost ?? 0),
                'icon' => 'question-circle', 'color' => 'danger', 'href' => route('assets.index', ['status' => AssetStatus::Lost->value]),
            ],
            'retired' => [
                'label' => 'Retired', 'value' => (int) ($assetCounts->retired ?? 0),
                'icon' => 'archive', 'color' => 'secondary', 'href' => route('assets.index', ['status' => AssetStatus::Retired->value]),
            ],
            'warranty_expiring' => [
                'label' => 'Warranty Expiring', 'value' => $warrantyExpiring,
                'icon' => 'hourglass-split', 'color' => 'warning', 'href' => route('warranty.index', ['status' => 'expiring', 'days' => 30]),
            ],
            'warranty_expired' => [
                'label' => 'Warranty Expired', 'value' => $warrantyExpired,
                'icon' => 'shield-x', 'color' => 'danger', 'href' => route('warranty.index', ['status' => 'expired']),
            ],
            'total_employees' => [
                'label' => 'Total Employees', 'value' => (int) ($employeeCounts->total ?? 0),
                'icon' => 'people', 'color' => 'info', 'href' => route('employees.index'),
            ],
            'active_employees' => [
                'label' => 'Active Employees', 'value' => (int) ($employeeCounts->active ?? 0),
                'icon' => 'person-check', 'color' => 'success', 'href' => route('employees.index', ['status' => EmployeeStatus::Active->value]),
            ],
        ];
    }

    /**
     * @return array{labels: list<string>, values: list<int>}
     */
    private function assetsByCategory(): array
    {
        $rows = Asset::query()
            ->join('asset_categories', 'asset_categories.id', '=', 'assets.category_id')
            ->selectRaw('asset_categories.name as label, COUNT(*) as total')
            ->groupBy('asset_categories.name')
            ->orderByDesc('total')
            ->get();

        return $this->topNWithOther($rows);
    }

    /**
     * @return array{labels: list<string>, values: list<int>}
     */
    private function assetsByTeam(): array
    {
        $rows = Asset::query()
            ->leftJoin('teams', 'teams.id', '=', 'assets.team_id')
            ->selectRaw("COALESCE(teams.name, 'No Team') as label, COUNT(*) as total")
            ->groupBy('label')
            ->orderByDesc('total')
            ->get();

        return $this->topNWithOther($rows);
    }

    /**
     * @return array{labels: list<string>, values: list<int>}
     */
    private function assetsByLocation(): array
    {
        $rows = Asset::query()
            ->leftJoin('locations', 'locations.id', '=', 'assets.location_id')
            ->selectRaw("COALESCE(locations.name, 'No Location') as label, COUNT(*) as total")
            ->groupBy('label')
            ->orderByDesc('total')
            ->get();

        return $this->topNWithOther($rows);
    }

    /**
     * @return array{labels: list<string>, values: list<int>, colors: list<string>}
     */
    private function assetStatusDistribution(): array
    {
        $counts = Asset::query()
            ->selectRaw('status, COUNT(*) as total')
            ->groupBy('status')
            ->pluck('total', 'status');

        $cases = collect(AssetStatus::cases())
            ->map(fn (AssetStatus $status): array => [
                'label' => $status->label(),
                'value' => (int) ($counts[$status->value] ?? 0),
                'color' => self::HEX[$status->color()],
            ])
            ->filter(fn (array $row): bool => $row['value'] > 0)
            ->values();

        return [
            'labels' => $cases->pluck('label')->all(),
            'values' => $cases->pluck('value')->all(),
            'colors' => $cases->pluck('color')->all(),
        ];
    }

    /**
     * Buckets every asset's *featured* coverage period only — an asset with
     * a closed, superseded warranty row but no primary one counts as "No
     * Coverage", never as whatever that old row's dates say. Business rule
     * carried over from Phase 7: dashboard widgets read the active warranty
     * only.
     *
     * @return array{labels: list<string>, values: list<int>, colors: list<string>}
     */
    private function warrantyStatus(): array
    {
        $buckets = [
            'Active' => 0,
            'Expiring Soon' => 0,
            'Expired' => 0,
            'No Coverage' => 0,
        ];

        AssetWarranty::query()->primary()->get()->each(function (AssetWarranty $warranty) use (&$buckets): void {
            $buckets[$warranty->status()->label()]++;
        });

        $buckets['No Coverage'] = Asset::query()->doesntHave('primaryWarranty')->count();

        $colors = [
            'Active' => self::HEX[WarrantyStatus::Active->color()],
            'Expiring Soon' => self::HEX[WarrantyStatus::ExpiringSoon->color()],
            'Expired' => self::HEX[WarrantyStatus::Expired->color()],
            'No Coverage' => self::HEX['secondary'],
        ];

        $rows = collect($buckets)
            ->map(fn (int $value, string $label): array => ['label' => $label, 'value' => $value, 'color' => $colors[$label]])
            ->filter(fn (array $row): bool => $row['value'] > 0)
            ->values();

        return [
            'labels' => $rows->pluck('label')->all(),
            'values' => $rows->pluck('value')->all(),
            'colors' => $rows->pluck('color')->all(),
        ];
    }

    /**
     * Asset count by purchase month for the trailing N months, zero-filled
     * so a quiet month shows as a gap rather than vanishing from the axis.
     *
     * @return array{labels: list<string>, values: list<int>}
     */
    private function monthlyProcurement(): array
    {
        $from = now()->startOfMonth()->subMonths(self::PROCUREMENT_MONTHS - 1);

        $rows = Asset::query()
            ->whereNotNull('purchase_date')
            ->where('purchase_date', '>=', $from->toDateString())
            ->selectRaw("DATE_FORMAT(purchase_date, '%Y-%m') as ym, COUNT(*) as total")
            ->groupBy('ym')
            ->pluck('total', 'ym');

        $labels = [];
        $values = [];

        for ($i = 0; $i < self::PROCUREMENT_MONTHS; $i++) {
            $month = $from->copy()->addMonths($i);
            $key = $month->format('Y-m');

            $labels[] = $month->format('M Y');
            $values[] = (int) ($rows[$key] ?? 0);
        }

        return ['labels' => $labels, 'values' => $values];
    }

    /**
     * @return Collection<int, AssetAssignment>
     */
    private function recentlyAssigned(): Collection
    {
        return AssetAssignment::query()
            ->with(['asset', 'employee'])
            ->orderByDesc('assigned_at')
            ->limit(self::WIDGET_LIMIT)
            ->get();
    }

    /**
     * @return Collection<int, AssetAssignment>
     */
    private function recentlyReturned(): Collection
    {
        return AssetAssignment::query()
            ->returned()
            ->with(['asset', 'employee'])
            ->orderByDesc('returned_at')
            ->limit(self::WIDGET_LIMIT)
            ->get();
    }

    /**
     * @return Collection<int, AuditLog>
     */
    private function recentActivity(): Collection
    {
        return AuditLog::query()
            ->with('user')
            ->latest()
            ->limit(self::WIDGET_LIMIT)
            ->get();
    }

    /**
     * @return Collection<int, AssetWarranty>
     */
    private function upcomingWarrantyExpiry(): Collection
    {
        return AssetWarranty::query()
            ->primary()
            ->with(['asset.category'])
            ->expiringWithin(90)
            ->orderBy('end_date')
            ->limit(self::WIDGET_LIMIT)
            ->get();
    }

    /**
     * @return Collection<int, AssetAssignment>
     */
    private function awaitingReturn(): Collection
    {
        return AssetAssignment::query()
            ->overdue()
            ->with(['asset', 'employee'])
            ->orderBy('expected_return_at')
            ->limit(self::WIDGET_LIMIT)
            ->get();
    }

    /**
     * @return Collection<int, Employee>
     */
    private function topHolders(): Collection
    {
        return Employee::query()
            ->with('team')
            ->withCount('activeAssignments')
            ->having('active_assignments_count', '>', 0)
            ->orderByDesc('active_assignments_count')
            ->limit(self::WIDGET_LIMIT)
            ->get();
    }

    /**
     * Collapses a label/total result set to the top N rows plus a single
     * "Other" row summing the rest — a chart with forty bars communicates
     * nothing a table wouldn't say better.
     *
     * @param  Collection<int, mixed>  $rows  rows with `label` and `total` attributes
     * @return array{labels: list<string>, values: list<int>}
     */
    private function topNWithOther(Collection $rows): array
    {
        $top = $rows->take(self::CHART_TOP_N);
        $rest = $rows->slice(self::CHART_TOP_N);

        $labels = $top->pluck('label')->all();
        $values = $top->pluck('total')->map(fn ($v): int => (int) $v)->all();

        if ($rest->isNotEmpty()) {
            $labels[] = 'Other';
            $values[] = (int) $rest->sum('total');
        }

        return ['labels' => $labels, 'values' => $values];
    }
}
