<?php

namespace App\Console\Commands;

use App\Console\Commands\Concerns\ResolvesBranchAdmins;
use App\Domain\Financial\Models\Payment;
use App\Domain\Notification\Services\NotificationService;
use App\Domain\Participant\Models\Participant;
use App\Domain\Shared\Models\Academy;
use App\Domain\Training\Models\Enrollment;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;

/**
 * Yesterday's takings, by email, to the people entitled to see them.
 *
 * Console runs with branch enforcement off (docs/agent-rules/19), which is the
 * right default for a cron — a job that silently reported one branch would be
 * worse than one that reads them all. But this command *sends*, and reading
 * every branch then mailing every admin put the whole academy's collections,
 * payment counts and new registrations in the inbox of a manager entitled to
 * one branch.
 *
 * So the figures are gathered across every branch, bucketed by branch, and each
 * recipient is sent the sum of exactly the branches their role assignment names
 * — the whole academy only for a role granted academy-wide.
 *
 * One message per recipient, never one per branch: NotificationService drops a
 * second email of the same event to the same user inside five minutes, so a
 * manager holding two branches would silently receive the first branch's
 * figures and never the second's.
 */
class SendDailySummary extends Command
{
    use ResolvesBranchAdmins;

    protected $signature = 'summary:daily';
    protected $description = 'Send daily summary to academy admins';

    /** Roles at or above this level receive the summary. */
    private const ADMIN_LEVEL = 80;

    /** Bucket key for rows whose branch_id is null — see summarise(). */
    private const UNATTRIBUTED = 0;

    public function handle(NotificationService $notificationService): int
    {
        $yesterday = now()->subDay()->toDateString();

        $academies = Academy::where('status', 'active')->get();

        foreach ($academies as $academy) {
            try {
                $this->summarise($academy, $yesterday, $notificationService);
            } catch (\Throwable $e) {
                Log::error("Daily summary failed for academy {$academy->id}: " . $e->getMessage());
                $this->error("Failed for {$academy->name}: " . $e->getMessage());
            }
        }

        return self::SUCCESS;
    }

    private function summarise(Academy $academy, string $date, NotificationService $notificationService): void
    {
        $buckets = $this->figuresByBranch($academy->id, $date);
        $names = $this->branchNames($academy->id);
        $admins = $this->branchAdmins($academy->id, self::ADMIN_LEVEL);

        // Money that belongs to no branch appears in the academy-wide total and
        // in no branch's, which is the honest reading — but it is also a data
        // fault worth seeing, because those rows are invisible on every
        // branch-scoped screen too.
        if (($buckets[self::UNATTRIBUTED]['count'] ?? 0) > 0) {
            $this->warn("  {$academy->name}: {$buckets[self::UNATTRIBUTED]['count']} payment(s) carry no branch — counted only in the academy-wide total.");
        }

        $sent = 0;

        foreach ($admins as $admin) {
            // academyWide is true exactly when one of their assignments has a
            // null branch_id, so a non-academy-wide admin always names at least
            // one branch here.
            $keys = $admin->academyWide ? array_keys($buckets) : $admin->branchIds;

            $notificationService->send(
                eventType: 'daily_summary',
                recipientType: 'user',
                recipientId: $admin->id,
                academyId: $academy->id,
                variables: $this->variables(
                    $date,
                    $this->merge($buckets, $keys),
                    $admin->academyWide ? __('كل الفروع') : $this->branchLabel($admin->branchIds, $names),
                ),
                recipientEmail: $admin->email,
            );

            $sent++;
        }

        $totals = $this->merge($buckets, array_keys($buckets));

        $this->info(
            "Sent daily summary for {$academy->name} to {$sent} recipient(s) "
            . "({$totals['count']} payments, " . number_format($totals['collected'] / 100, 2) . " EGP academy-wide)"
        );
    }

    // ------------------------------------------------------------- figures

    /**
     * Yesterday's numbers, bucketed by branch. Key 0 holds rows with no branch.
     *
     * @return array<int, array<string, mixed>>
     */
    private function figuresByBranch(int $academyId, string $date): array
    {
        $buckets = [];

        $payments = Payment::where('academy_id', $academyId)
            ->where('status', 'confirmed')
            ->whereDate('payment_date', $date)
            ->get(['branch_id', 'amount', 'method']);

        foreach ($payments as $payment) {
            $key = (int) $payment->branch_id;
            $method = $payment->method?->value ?? 'other';

            $buckets[$key] ??= $this->emptyBucket();
            $buckets[$key]['collected'] += (int) $payment->amount;
            $buckets[$key]['count']++;
            $buckets[$key]['methods'][$method] = ($buckets[$key]['methods'][$method] ?? 0) + (int) $payment->amount;
        }

        $counts = [
            'enrollments' => $this->countByBranch(
                Enrollment::where('academy_id', $academyId)->whereDate('enrollment_date', $date)
            ),
            'participants' => $this->countByBranch(
                Participant::where('academy_id', $academyId)->whereDate('created_at', $date)
            ),
        ];

        foreach ($counts as $field => $byBranch) {
            foreach ($byBranch as $key => $count) {
                $buckets[$key] ??= $this->emptyBucket();
                $buckets[$key][$field] = $count;
            }
        }

        return $buckets;
    }

    /**
     * COUNT(*) grouped by branch, with null collapsing into key 0.
     *
     * @return array<int, int>
     */
    private function countByBranch(Builder $query): array
    {
        $rows = $query->toBase()
            ->selectRaw('branch_id, COUNT(*) AS aggregate')
            ->groupBy('branch_id')
            ->get();

        $out = [];

        foreach ($rows as $row) {
            $out[(int) $row->branch_id] = (int) $row->aggregate;
        }

        return $out;
    }

    /**
     * @return array<string, mixed>
     */
    private function emptyBucket(): array
    {
        return [
            'collected' => 0,
            'count' => 0,
            'methods' => [],
            'enrollments' => 0,
            'participants' => 0,
        ];
    }

    /**
     * Sum the named buckets into one. A branch with no activity contributes
     * zeros rather than disappearing, so a quiet branch still gets its summary.
     *
     * @param  array<int, array<string, mixed>>  $buckets
     * @param  array<int, int>  $keys
     * @return array<string, mixed>
     */
    private function merge(array $buckets, array $keys): array
    {
        $merged = $this->emptyBucket();

        foreach ($keys as $key) {
            $bucket = $buckets[(int) $key] ?? null;

            if (! $bucket) {
                continue;
            }

            $merged['collected'] += $bucket['collected'];
            $merged['count'] += $bucket['count'];
            $merged['enrollments'] += $bucket['enrollments'];
            $merged['participants'] += $bucket['participants'];

            foreach ($bucket['methods'] as $method => $amount) {
                $merged['methods'][$method] = ($merged['methods'][$method] ?? 0) + $amount;
            }
        }

        return $merged;
    }

    // ------------------------------------------------------------ rendering

    /**
     * @param  array<string, mixed>  $bucket
     * @return array<string, string>
     */
    private function variables(string $date, array $bucket, string $branchLabel): array
    {
        return [
            'date' => $date,
            'branch_name' => $branchLabel,
            'total_collected' => number_format($bucket['collected'] / 100, 2),
            'payment_count' => (string) $bucket['count'],
            'new_enrollments' => (string) $bucket['enrollments'],
            'new_participants' => (string) $bucket['participants'],
            'cash_total' => number_format(($bucket['methods']['cash'] ?? 0) / 100, 2),
            'card_total' => number_format(($bucket['methods']['card'] ?? 0) / 100, 2),
        ];
    }
}
