<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Services\Dashboard\SupplyReconciliation;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * SaaS-admin «Командный центр» — read-only агрегаты для дашборда.
 * Под группой ['saas-admin','admin-db'] → cross-tenant через pgsql_admin.
 * Spec: docs/superpowers/specs/2026-06-27-admin-command-center-design.md
 */
class AdminDashboardController extends Controller
{
    /**
     * Диапазон периода из query: либо date_from/date_to (свой период, приоритет),
     * либо preset period=today|7d|30d|60d|90d (дефолт 7d). Возвращает [from, to]:
     * to — верхняя граница (конец дня date_to при своём периоде, иначе now).
     *
     * @return array{0:Carbon,1:Carbon}
     */
    private function periodRange(Request $request): array
    {
        $df = (string) $request->query('date_from', '');
        $dt = (string) $request->query('date_to', '');
        if ($df !== '' && $dt !== '') {
            try {
                return [Carbon::parse($df)->startOfDay(), Carbon::parse($dt)->endOfDay()];
            } catch (\Throwable) {
                // невалидные даты → падаем на preset ниже
            }
        }

        $from = match ((string) $request->query('period', '7d')) {
            'today' => now()->startOfDay(),
            '30d' => now()->subDays(30),
            '60d' => now()->subDays(60),
            '90d' => now()->subDays(90),
            default => now()->subDays(7),
        };

        return [$from, now()];
    }

    /** GET /api/admin/dashboard — сводка L1 (все плитки). */
    public function summary(Request $request): JsonResponse
    {
        [$from, $to] = $this->periodRange($request);

        return response()->json([
            'period' => (string) $request->query('period', '7d'),
            'date_from' => $request->query('date_from'),
            'date_to' => $request->query('date_to'),
            'finance' => $this->financeTile($from, $to),
            'health' => $this->healthTile(),
            'leads' => $this->leadsTile(),
            'supply' => $this->supplyTile(),
            'balances' => $this->balancesTile(),
            'clients' => $this->clientsTile($from, $to),
        ]);
    }

    /** @return array<string,mixed> */
    private function financeTile(Carbon $from, Carbon $to): array
    {
        $topups = (float) DB::table('balance_transactions')
            ->where('type', 'topup')->whereBetween('created_at', [$from, $to])->sum('amount_rub');
        $charges = (float) DB::table('balance_transactions')
            ->where('type', 'lead_charge')->whereBetween('created_at', [$from, $to])->sum('amount_rub');
        $active = DB::table('tenants')->where('status', 'active')->whereNull('deleted_at')->count();
        $newClients = DB::table('tenants')->whereBetween('created_at', [$from, $to])->whereNull('deleted_at')->count();
        $negative = DB::table('tenants')->whereNull('deleted_at')->where('balance_rub', '<', 0)->count();

        return [
            'topups_rub' => (string) $topups,
            'charges_rub' => (string) abs($charges),
            'active_clients' => $active,
            'new_clients' => $newClients,
            'negative_balance_count' => $negative,
            'light' => $negative > 0 ? 'red' : 'green',
        ];
    }

    /** GET /api/admin/dashboard/finance — детали Финансов (L2). */
    public function finance(Request $request): JsonResponse
    {
        [$from, $to] = $this->periodRange($request);

        $topups = (float) DB::table('balance_transactions')
            ->where('type', 'topup')->whereBetween('created_at', [$from, $to])->sum('amount_rub');
        $charges = abs((float) DB::table('balance_transactions')
            ->where('type', 'lead_charge')->whereBetween('created_at', [$from, $to])->sum('amount_rub'));

        // «Требуют внимания»: баланс < 0 (по возрастанию — самые глубокие минусы сверху).
        $attention = DB::table('tenants')->whereNull('deleted_at')
            ->where('balance_rub', '<', 0)
            ->orderBy('balance_rub')
            ->limit(20)
            ->get(['id', 'subdomain', 'organization_name', 'balance_rub', 'balance_leads'])
            ->map(fn ($t) => [
                'id' => (int) $t->id,
                'subdomain' => $t->subdomain,
                'organization_name' => $t->organization_name,
                'balance_rub' => (string) $t->balance_rub,
                'state' => 'negative',
            ]);

        // Топ по обороту: сумма пополнений за период.
        $top = DB::table('balance_transactions')
            ->join('tenants', 'tenants.id', '=', 'balance_transactions.tenant_id')
            ->where('balance_transactions.type', 'topup')
            ->whereBetween('balance_transactions.created_at', [$from, $to])
            ->whereNull('tenants.deleted_at')
            ->groupBy('tenants.id', 'tenants.organization_name')
            ->orderByRaw('SUM(balance_transactions.amount_rub) DESC')
            ->limit(10)
            ->get([
                'tenants.id',
                'tenants.organization_name',
                DB::raw('SUM(balance_transactions.amount_rub) AS topped_rub'),
            ])
            ->map(fn ($r) => [
                'id' => (int) $r->id,
                'organization_name' => $r->organization_name,
                'topped_rub' => (string) $r->topped_rub,
            ]);

        return response()->json([
            'period' => (string) $request->query('period', '7d'),
            'kpi' => [
                'topups_rub' => (string) $topups,
                'charges_rub' => (string) $charges,
                'net_inflow_rub' => (string) ($topups - $charges),
                'negative_balance_count' => $attention->count(),
            ],
            'attention' => $attention,
            'top_by_turnover' => $top,
        ]);
    }

    /** GET /api/admin/dashboard/health — 6 подсистем эксплуатации (L2). */
    public function health(): JsonResponse
    {
        $failedJobs = DB::table('failed_jobs')->where('failed_at', '>=', now()->subDay())->count();
        $lastSync = DB::table('supplier_sync_runs')->orderByDesc('id')->first();
        $lastReconcile = DB::table('supplier_csv_reconcile_log')->orderByDesc('id')->first();
        $unresolvedWebhooks = DB::table('failed_webhook_jobs')->whereNull('resolved_at')->count();
        $inc = $this->incidentCounts();
        $staleHeartbeat = DB::table('scheduler_heartbeats')->where('consecutive_failures', '>', 0)->count();

        $jobsLight = ($failedJobs > 0 || $inc['auto_job_24h'] > 0) ? 'red' : 'green';
        $jobsDetail = $inc['auto_job_24h'] > 0
            ? $inc['auto_job_24h'].' повторяющихся ошибок джоб за сутки'
            : $failedJobs.' упавших за сутки';

        $subsystems = [
            ['key' => 'queues', 'light' => $jobsLight, 'detail' => $jobsDetail],
            ['key' => 'scheduler', 'light' => $staleHeartbeat > 0 ? 'red' : 'green',
                'detail' => $staleHeartbeat > 0 ? $staleHeartbeat.' задач с пропусками' : 'по расписанию'],
            ['key' => 'supplier_sync', 'light' => ($lastSync && in_array($lastSync->status, ['failed', 'aborted'], true)) ? 'red' : 'green',
                'detail' => 'последний: '.($lastSync->status ?? 'нет')],
            ['key' => 'csv_drift', 'light' => ($lastReconcile && $lastReconcile->status === 'drift_alert') ? 'red' : 'green',
                'detail' => 'статус: '.($lastReconcile->status ?? 'нет')],
            ['key' => 'webhooks', 'light' => $unresolvedWebhooks > 0 ? 'amber' : 'green',
                'detail' => $unresolvedWebhooks.' неразобранных'],
            ['key' => 'incidents', 'light' => $inc['real'] > 0 ? 'red' : 'green',
                'detail' => $inc['real'].' открытых (реальных)'],
        ];

        $order = ['green' => 0, 'amber' => 1, 'red' => 2];
        $overall = collect($subsystems)->sortByDesc(fn ($s) => $order[$s['light']])->first()['light'];

        return response()->json(['subsystems' => $subsystems, 'overall_light' => $overall]);
    }

    /**
     * Счётчики инцидентов с разделением: РЕАЛЬНЫЕ (заведённые человеком/РКН) vs
     * АВТО-ошибки джоб ('Автоматически: persistent exception job=…'), которые
     * копятся и сами не закрываются. Для здоровья считаем реальные + свежие авто.
     *
     * @return array{real:int,auto_job_24h:int}
     */
    private function incidentCounts(): array
    {
        $real = DB::table('incidents_log')->whereNull('resolved_at')
            ->where(function ($q) {
                $q->whereNull('summary')->orWhere('summary', 'not like', 'Автоматически:%');
            })
            ->count();

        $autoJob24h = DB::table('incidents_log')->whereNull('resolved_at')
            ->where('summary', 'like', 'Автоматически:%')
            ->where('detected_at', '>=', now()->subDay())
            ->count();

        return ['real' => $real, 'auto_job_24h' => $autoJob24h];
    }

    /** @return array<string,mixed> */
    private function healthTile(): array
    {
        $inc = $this->incidentCounts();
        $lastSync = DB::table('supplier_sync_runs')->orderByDesc('id')->first();
        $failedJobs = DB::table('failed_jobs')->where('failed_at', '>=', now()->subDay())->count();

        $light = 'green';
        if ($inc['real'] > 0 || $failedJobs > 0 || $inc['auto_job_24h'] > 0
            || ($lastSync !== null && in_array($lastSync->status, ['failed', 'aborted'], true))) {
            $light = 'red';
        }

        return [
            'light' => $light,
            'open_incidents' => $inc['real'],
            'job_errors_24h' => $inc['auto_job_24h'],
            'failed_jobs_24h' => $failedJobs,
            'last_sync_status' => $lastSync->status ?? 'none',
            'last_sync_at' => $lastSync->finished_at ?? null,
        ];
    }

    // === Этап 2: Лиды ===

    /** @return array<string,mixed> */
    private function leadsMetrics(): array
    {
        $todayStart = now('Europe/Moscow')->startOfDay();

        // Доставлено = реально созданные сегодня сделки у клиентов (не тест, не удал.).
        $deliveredToday = DB::table('deals')
            ->where('received_at', '>=', $todayStart)
            ->where('is_test', false)
            ->whereNull('deleted_at')
            ->count();
        // Получено от поставщика сегодня.
        $receivedToday = DB::table('supplier_leads')->where('received_at', '>=', $todayStart)->count();
        // В очереди на распределение прямо сейчас.
        $unrouted = DB::table('supplier_leads')->whereNull('processed_at')->count();
        // Зависшие = не распределены дольше 4 часов (порог cron leads:escalate-stale).
        $stuck = DB::table('supplier_leads')
            ->whereNull('processed_at')
            ->where('received_at', '<', now()->subHours(4))
            ->count();

        $light = 'green';
        if ($stuck > 0) {
            $light = 'red';
        } elseif ($unrouted > 0) {
            $light = 'amber';
        }

        return [
            'light' => $light,
            'delivered_today' => $deliveredToday,
            'received_today' => $receivedToday,
            'stuck' => $stuck,
            'unrouted' => $unrouted,
        ];
    }

    /** @return array<string,mixed> */
    private function leadsTile(): array
    {
        $m = $this->leadsMetrics();

        return [
            'light' => $m['light'],
            'delivered_today' => $m['delivered_today'],
            'received_today' => $m['received_today'],
            'stuck' => $m['stuck'],
            'unrouted' => $m['unrouted'],
        ];
    }

    /** GET /api/admin/dashboard/leads — KPI распределения лидов + топ-10 последних (L2). */
    public function leads(): JsonResponse
    {
        $m = $this->leadsMetrics();

        // Топ-10 последних лидов для drill (полный список — на экране /admin/leads).
        $recent = DB::table('supplier_leads as sl')
            ->leftJoin('supplier_projects as sp', 'sp.id', '=', 'sl.supplier_project_id')
            ->orderByDesc('sl.received_at')
            ->limit(10)
            ->get(['sl.id', 'sl.received_at', 'sl.platform', 'sl.phone', 'sl.processed_at',
                'sl.deals_created_count', 'sp.signal_type as channel', 'sp.unique_key'])
            ->map(fn ($r) => [
                'id' => (int) $r->id,
                'received_at' => $r->received_at,
                'platform' => $r->platform,
                'channel' => $r->channel,
                'source' => $r->unique_key,
                'phone_masked' => $this->maskPhoneShort($r->phone),
                'delivered' => ((int) ($r->deals_created_count ?? 0)) > 0,
                'processed' => $r->processed_at !== null,
            ]);

        return response()->json([
            'light' => $m['light'],
            'kpi' => [
                'delivered_today' => $m['delivered_today'],
                'received_today' => $m['received_today'],
                'stuck' => $m['stuck'],
                'unrouted' => $m['unrouted'],
            ],
            'recent' => $recent,
        ]);
    }

    /** Короткая маска телефона для drill (152-ФЗ). */
    private function maskPhoneShort(?string $phone): string
    {
        if (! $phone) {
            return '—';
        }
        $d = preg_replace('/\D/', '', $phone);

        return strlen((string) $d) >= 4 ? substr((string) $d, 0, 2).'***'.substr((string) $d, -2) : '***';
    }

    // === Этап 2: Заказ у поставщика ===

    /**
     * Сырьё для сверки заказа: спрос (последний снимок) + факт (supplier_projects).
     * Плюс ПОЛНАЯ картина у поставщика (все активные заказы), чтобы не выглядело
     * занижено: сверка идёт только по группам последнего снимка, а заказов больше.
     *
     * @return array{snapshot_date:?string,total_orders:int,total_limit:int,result:array{groups:list<array<string,mixed>>,totals:array<string,int>}}
     */
    private function supplyReconciliation(): array
    {
        /** @var string|null $latest */
        $latest = DB::table('project_routing_snapshots')->max('snapshot_date');

        $demand = [];
        if ($latest !== null) {
            $rows = DB::table('project_routing_snapshots')
                ->where('snapshot_date', $latest)
                ->groupBy('signal_type', 'signal_identifier')
                ->select(
                    'signal_type',
                    'signal_identifier',
                    DB::raw('SUM(daily_limit) AS demand'),
                    DB::raw('MAX(daily_limit) AS max_limit'),
                )
                ->get();

            foreach ($rows as $r) {
                $demand[] = [
                    'signal_type' => (string) $r->signal_type,
                    'identifier' => (string) $r->signal_identifier,
                    'demand' => (int) $r->demand,
                    'max_limit' => (int) $r->max_limit,
                ];
            }
        }

        /** @var array<string,int> $orderedByKey */
        $orderedByKey = DB::table('supplier_projects')
            ->groupBy('signal_type', 'unique_key')
            ->select('signal_type', 'unique_key', DB::raw('SUM(current_limit) AS ordered'))
            ->get()
            ->mapWithKeys(fn ($r) => [$r->signal_type.'|'.$r->unique_key => (int) $r->ordered])
            ->all();

        return [
            'snapshot_date' => $latest,
            'total_orders' => (int) DB::table('supplier_projects')->where('current_limit', '>', 0)->count(),
            'total_limit' => (int) DB::table('supplier_projects')->sum('current_limit'),
            'result' => SupplyReconciliation::build($demand, $orderedByKey),
        ];
    }

    /** @return array<string,mixed> */
    private function supplyTile(): array
    {
        $rec = $this->supplyReconciliation();
        $totals = $rec['result']['totals'];

        return [
            'light' => $totals['mismatches'] > 0 ? 'red' : 'green',
            'demand' => $totals['demand'],
            'formula' => $totals['formula'],
            'ordered' => $totals['ordered'],
            'mismatches' => $totals['mismatches'],
            'total_orders' => $rec['total_orders'],
            'total_limit' => $rec['total_limit'],
            'snapshot_date' => $rec['snapshot_date'],
        ];
    }

    // === Балансы внешних сервисов (28.06) ===

    /** Порядок «опасности» светофора: больше = хуже. */
    private const LIGHT_ORDER = ['green' => 0, 'grey' => 1, 'amber' => 2, 'red' => 3];

    /**
     * Прямая ссылка «Пополнить» для сервиса (статика из конфига; в БД не хранится).
     * Владелец с планшета: увидел минус → ткнул → попал на страницу оплаты.
     */
    private function topupUrl(string $key): ?string
    {
        return match ($key) {
            'dadata' => (string) config('services.dadata.topup_url') ?: null,
            'supplier' => (string) config('services.supplier.topup_url') ?: null,
            'yandex_cloud' => $this->ycTopupUrl(),
            default => null,
        };
    }

    private function ycTopupUrl(): ?string
    {
        $base = (string) config('services.yandex_cloud.console_billing_url');
        $acc = (string) config('services.yandex_cloud.billing_account_id');
        if ($base === '' || $acc === '') {
            return null;
        }

        return rtrim($base, '/').'/'.$acc.'/payments';
    }

    /** @return array<string,mixed> */
    private function balancesTile(): array
    {
        $rows = DB::table('external_service_balances')->get();
        $light = $rows->isEmpty() ? 'grey'
            : $rows->map(fn ($r) => $r->ok ? $r->light : 'grey')
                ->sortByDesc(fn ($l) => self::LIGHT_ORDER[$l] ?? 0)->first();

        return [
            'light' => $light,
            'count' => $rows->count(),
            'red' => $rows->where('ok', true)->where('light', 'red')->count(),
        ];
    }

    /** GET /api/admin/dashboard/balances — балансы внешних сервисов (L2). */
    public function balances(): JsonResponse
    {
        $rows = DB::table('external_service_balances')->get()->map(fn ($r) => [
            'service_key' => $r->service_key,
            'balance_amount' => $r->balance_amount,
            'currency' => $r->currency,
            'daily_spend_estimate' => $r->daily_spend_estimate,
            'days_left' => $r->days_left,
            'light' => $r->ok ? $r->light : 'grey',
            'ok' => (bool) $r->ok,
            'error' => $r->error,
            'checked_at' => $r->checked_at,
            'topup_url' => $this->topupUrl($r->service_key),
        ])->values();

        $light = $rows->isEmpty() ? 'grey'
            : $rows->sortByDesc(fn ($s) => self::LIGHT_ORDER[$s['light']] ?? 0)->first()['light'];

        return response()->json(['light' => $light, 'services' => $rows]);
    }

    // === Клиенты (активность) ===

    /** Клиент «спит», если его тенант не заходил дольше этого срока (или ни разу). */
    private const DORMANT_DAYS = 14;

    /** @return array{total_active:int,new_count:int,logged_in:int,got_leads:int,paid:int} */
    private function clientActivityKpi(Carbon $from, Carbon $to): array
    {
        return [
            'total_active' => DB::table('tenants')->whereNull('deleted_at')->where('status', 'active')->count(),
            'new_count' => DB::table('tenants')->whereNull('deleted_at')->whereBetween('created_at', [$from, $to])->count(),
            'logged_in' => DB::table('users')->whereBetween('last_login_at', [$from, $to])->distinct()->count('tenant_id'),
            'got_leads' => DB::table('deals')->whereBetween('received_at', [$from, $to])->where('is_test', false)
                ->whereNull('deleted_at')->distinct()->count('tenant_id'),
            'paid' => DB::table('balance_transactions')->where('type', 'topup')->whereBetween('created_at', [$from, $to])
                ->distinct()->count('tenant_id'),
        ];
    }

    /** Активные тенанты без входа дольше DORMANT_DAYS (или ни разу) — «спящие». */
    private function dormantQuery(): Builder
    {
        $lastLogin = DB::table('users')->select('tenant_id', DB::raw('MAX(last_login_at) as last_login_at'))
            ->groupBy('tenant_id');

        return DB::table('tenants')
            ->leftJoinSub($lastLogin, 'll', 'll.tenant_id', '=', 'tenants.id')
            ->whereNull('tenants.deleted_at')
            ->where('tenants.status', 'active')
            ->where(function ($q) {
                $q->whereNull('ll.last_login_at')
                    ->orWhere('ll.last_login_at', '<', now()->subDays(self::DORMANT_DAYS));
            });
    }

    /** @return array<string,mixed> */
    private function clientsTile(Carbon $from, Carbon $to): array
    {
        $kpi = $this->clientActivityKpi($from, $to);
        $dormant = (clone $this->dormantQuery())->count();

        return [
            'light' => $dormant > 0 ? 'amber' : 'green',
            'total_active' => $kpi['total_active'],
            'new_count' => $kpi['new_count'],
            'logged_in' => $kpi['logged_in'],
            'dormant' => $dormant,
        ];
    }

    /** GET /api/admin/dashboard/clients — активность клиентов + новые + спящие (L2). */
    public function clients(Request $request): JsonResponse
    {
        [$from, $to] = $this->periodRange($request);
        $kpi = $this->clientActivityKpi($from, $to);

        $lastLogin = DB::table('users')->select('tenant_id', DB::raw('MAX(last_login_at) as last_login_at'))
            ->groupBy('tenant_id');

        $newClients = DB::table('tenants')
            ->leftJoinSub($lastLogin, 'll', 'll.tenant_id', '=', 'tenants.id')
            ->whereNull('tenants.deleted_at')
            ->whereBetween('tenants.created_at', [$from, $to])
            ->orderByDesc('tenants.created_at')
            ->limit(50)
            ->get([
                'tenants.id', 'tenants.organization_name', 'tenants.subdomain', 'tenants.status',
                'tenants.created_at', 'tenants.balance_rub', 'tenants.delivered_in_month', 'll.last_login_at',
            ])
            ->map(fn ($t) => [
                'id' => (int) $t->id,
                'organization_name' => $t->organization_name ?: $t->subdomain,
                'subdomain' => $t->subdomain,
                'status' => $t->status,
                'created_at' => $t->created_at,
                'last_login_at' => $t->last_login_at,
                'delivered_in_month' => (int) $t->delivered_in_month,
                'balance_rub' => (string) $t->balance_rub,
            ]);

        $dormant = (clone $this->dormantQuery())
            ->orderByRaw('ll.last_login_at ASC NULLS FIRST')
            ->limit(50)
            ->get(['tenants.id', 'tenants.organization_name', 'tenants.subdomain', 'tenants.balance_rub', 'll.last_login_at'])
            ->map(fn ($t) => [
                'id' => (int) $t->id,
                'organization_name' => $t->organization_name ?: $t->subdomain,
                'subdomain' => $t->subdomain,
                'last_login_at' => $t->last_login_at,
                'balance_rub' => (string) $t->balance_rub,
            ]);

        return response()->json([
            'kpi' => $kpi,
            'new_clients' => $newClients,
            'dormant' => $dormant,
        ]);
    }

    /** GET /api/admin/dashboard/supply — заказ у поставщика по группам (L2). */
    public function supply(): JsonResponse
    {
        $rec = $this->supplyReconciliation();
        $totals = $rec['result']['totals'];

        return response()->json([
            'snapshot_date' => $rec['snapshot_date'],
            'light' => $totals['mismatches'] > 0 ? 'red' : 'green',
            'totals' => $totals,
            'total_orders' => $rec['total_orders'],
            'total_limit' => $rec['total_limit'],
            'groups' => $rec['result']['groups'],
        ]);
    }
}
