<?php

namespace App\Http\Controllers\Api\V1\Admin;

use App\Enums\QueueStatus;
use App\Enums\TransactionStatus;
use App\Enums\TransactionType;
use App\Exceptions\DomainException;
use App\Http\Controllers\Controller;
use App\Models\Appointment;
use App\Models\AuditLog;
use App\Models\DoctorLocationAssignment;
use App\Models\NotificationLog;
use App\Models\Patient;
use App\Models\PaymentTransaction;
use App\Models\QueueEntry;
use App\Services\ScheduleService;
use App\Support\ApiResponse;
use Carbon\CarbonImmutable;
use Carbon\CarbonPeriod;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;

class ReportController extends Controller
{
    use ApiResponse;

    public function __construct(private readonly ScheduleService $schedule) {}

    private function range(Request $request): array
    {
        $request->validate([
            'date_from' => ['nullable', 'date'],
            'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
            'doctor_id' => ['nullable', 'integer'],
            'location_id' => ['nullable', 'integer'],
        ]);

        $from = $request->filled('date_from')
            ? CarbonImmutable::parse($request->input('date_from'))->startOfDay()
            : CarbonImmutable::now()->subDays(30)->startOfDay();
        $to = $request->filled('date_to')
            ? CarbonImmutable::parse($request->input('date_to'))->endOfDay()
            : CarbonImmutable::now()->endOfDay();

        if ($from->diffInDays($to) > 366) {
            throw new DomainException('REPORT_RANGE_TOO_LARGE', 'Report date range cannot exceed 366 days.', 422);
        }

        return [$from, $to];
    }

    public function appointments(Request $request)
    {
        return $this->success($this->appointmentRows($request));
    }

    private function appointmentRows(Request $request): Collection
    {
        [$from, $to] = $this->range($request);
        $query = Appointment::query()
            ->selectRaw('DATE(scheduled_start_at) report_date, doctor_id, location_id, status, COUNT(*) total')
            ->where('organization_id', $request->user()->organization_id)
            ->whereBetween('scheduled_start_at', [$from, $to]);

        $this->applyDoctorLocationFilters($query, $request);

        return $query
            ->groupByRaw('DATE(scheduled_start_at), doctor_id, location_id, status')
            ->orderBy('report_date')
            ->get();
    }

    public function queue(Request $request)
    {
        [$from, $to] = $this->range($request);
        $query = QueueEntry::query()
            ->where('organization_id', $request->user()->organization_id)
            ->whereBetween('queue_date', [$from->toDateString(), $to->toDateString()]);

        $this->applyDoctorLocationFilters($query, $request);

        $rows = $query->get()
            ->groupBy(fn ($entry) => $entry->queue_date->toDateString().'|'.$entry->doctor_id.'|'.$entry->location_id)
            ->map(function ($entries) {
                $first = $entries->first();
                $waits = $entries->filter(fn ($e) => $e->arrival_at && $e->sent_to_doctor_at)
                    ->map(fn ($e) => $e->arrival_at->diffInMinutes($e->sent_to_doctor_at));

                return [
                    'date' => $first->queue_date->toDateString(),
                    'doctor_id' => $first->doctor_id,
                    'location_id' => $first->location_id,
                    'tokens_issued' => $entries->count(),
                    'completed' => $entries->where('status', QueueStatus::COMPLETED)->count(),
                    'no_shows' => $entries->where('status', QueueStatus::NO_SHOW)->count(),
                    'average_wait_minutes' => $waits->count() ? round((float) $waits->avg(), 1) : 0,
                    'max_wait_minutes' => $waits->count() ? $waits->max() : 0,
                ];
            })->values();

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

    public function revenue(Request $request)
    {
        return $this->success($this->revenueRows($request));
    }

    private function revenueRows(Request $request): Collection
    {
        [$from, $to] = $this->range($request);

        $query = PaymentTransaction::query()
            ->from('transactions as t')
            ->join('appointments as a', 'a.id', '=', 't.appointment_id')
            ->selectRaw('DATE(t.created_at) report_date, a.doctor_id, t.location_id, t.method, t.type, SUM(t.amount) total_amount, COUNT(*) transactions_count')
            ->where('t.organization_id', $request->user()->organization_id)
            ->where('t.status', TransactionStatus::SUCCEEDED->value)
            ->whereBetween('t.created_at', [$from, $to]);

        if ($request->filled('doctor_id')) {
            $query->where('a.doctor_id', $request->integer('doctor_id'));
        }
        if ($request->filled('location_id')) {
            $query->where('t.location_id', $request->integer('location_id'));
        }

        return $query
            ->groupByRaw('DATE(t.created_at), a.doctor_id, t.location_id, t.method, t.type')
            ->orderBy('report_date')
            ->get();
    }

    public function doctorUtilization(Request $request)
    {
        [$from, $to] = $this->range($request);
        $organizationId = $request->user()->organization_id;

        $assignments = DoctorLocationAssignment::query()
            ->with(['doctor.user', 'doctor.organization', 'location', 'workingHours', 'scheduleExceptions'])
            ->where('organization_id', $organizationId)
            ->where('is_active', true)
            ->when($request->filled('doctor_id'), fn ($q) => $q->where('doctor_id', $request->integer('doctor_id')))
            ->when($request->filled('location_id'), fn ($q) => $q->where('location_id', $request->integer('location_id')))
            ->get();

        $rows = $assignments->map(function (DoctorLocationAssignment $assignment) use ($from, $to, $organizationId) {
            $capacity = 0;
            foreach (CarbonPeriod::create($from->startOfDay(), $to->startOfDay()) as $day) {
                $capacity += $this->schedule->capacitySlotsForDate($assignment, $day->toDateString());
            }

            $appointments = Appointment::query()
                ->where('organization_id', $organizationId)
                ->where('doctor_id', $assignment->doctor_id)
                ->where('location_id', $assignment->location_id)
                ->whereBetween('scheduled_start_at', [$from, $to]);

            $total = (clone $appointments)->count();
            $booked = (clone $appointments)->whereNotIn('status', ['cancelled', 'no_show'])->count();
            $completed = (clone $appointments)->where('status', 'completed')->count();

            return [
                'doctor_id' => $assignment->doctor_id,
                'doctor_name' => $assignment->doctor?->user?->name,
                'location_id' => $assignment->location_id,
                'location_name' => $assignment->location?->name,
                'slot_minutes' => $assignment->slot_minutes,
                'capacity_slots' => $capacity,
                'appointments_total' => $total,
                'appointments_booked' => $booked,
                'appointments_completed' => $completed,
                'utilization_percent' => $capacity > 0 ? round(($booked / $capacity) * 100, 2) : 0,
            ];
        });

        return $this->success($rows->values());
    }

    public function outstandingPayments(Request $request)
    {
        [$from, $to] = $this->range($request);

        $query = Appointment::query()
            ->from('appointments as a')
            ->leftJoin('transactions as t', function ($join) {
                $join->on('t.appointment_id', '=', 'a.id')
                    ->where('t.status', '=', TransactionStatus::SUCCEEDED->value);
            })
            ->join('patients as p', 'p.id', '=', 'a.patient_id')
            ->selectRaw('a.id appointment_id, a.appointment_code, a.patient_id, p.name patient_name, a.doctor_id, a.location_id, a.final_fee, a.payment_status, COALESCE(SUM(t.amount), 0) applied_amount, CASE WHEN a.final_fee - COALESCE(SUM(t.amount), 0) > 0 THEN a.final_fee - COALESCE(SUM(t.amount), 0) ELSE 0 END outstanding_amount')
            ->where('a.organization_id', $request->user()->organization_id)
            ->whereBetween('a.scheduled_start_at', [$from, $to]);

        if ($request->filled('doctor_id')) {
            $query->where('a.doctor_id', $request->integer('doctor_id'));
        }
        if ($request->filled('location_id')) {
            $query->where('a.location_id', $request->integer('location_id'));
        }

        $rows = $query
            ->groupBy('a.id', 'a.appointment_code', 'a.patient_id', 'p.name', 'a.doctor_id', 'a.location_id', 'a.final_fee', 'a.payment_status')
            ->havingRaw('(a.final_fee - COALESCE(SUM(t.amount), 0)) > 0')
            ->orderByDesc('outstanding_amount')
            ->get();

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

    public function refunds(Request $request)
    {
        [$from, $to] = $this->range($request);

        $query = PaymentTransaction::query()
            ->with(['appointment.doctor.user', 'appointment.patient', 'appointment.location'])
            ->where('organization_id', $request->user()->organization_id)
            ->where('type', TransactionType::REFUND->value)
            ->where('status', TransactionStatus::SUCCEEDED->value)
            ->whereBetween('created_at', [$from, $to]);

        if ($request->filled('doctor_id')) {
            $query->whereHas('appointment', fn ($q) => $q->where('doctor_id', $request->integer('doctor_id')));
        }
        if ($request->filled('location_id')) {
            $query->where('location_id', $request->integer('location_id'));
        }

        return $this->success($query->orderByDesc('created_at')->get());
    }

    public function patients(Request $request)
    {
        [$from, $to] = $this->range($request);
        $organizationId = $request->user()->organization_id;

        $newPatients = Patient::query()
            ->where('organization_id', $organizationId)
            ->whereBetween('created_at', [$from, $to])
            ->count();

        $completedVisits = Appointment::query()
            ->where('organization_id', $organizationId)
            ->where('status', 'completed')
            ->whereBetween('scheduled_start_at', [$from, $to])
            ->count();

        $returningPatients = Appointment::query()
            ->where('organization_id', $organizationId)
            ->where('status', 'completed')
            ->whereBetween('scheduled_start_at', [$from, $to])
            ->whereIn('patient_id', function ($sub) use ($organizationId, $from) {
                $sub->select('patient_id')
                    ->from('appointments')
                    ->where('organization_id', $organizationId)
                    ->where('status', 'completed')
                    ->where('scheduled_start_at', '<', $from)
                    ->distinct();
            })
            ->distinct()
            ->count('patient_id');

        return $this->success([
            'new_patients' => $newPatients,
            'completed_visits' => $completedVisits,
            'returning_patients' => $returningPatients,
        ]);
    }

    public function cancellations(Request $request)
    {
        [$from, $to] = $this->range($request);

        $query = Appointment::query()
            ->selectRaw('doctor_id, location_id, status, COUNT(*) total')
            ->where('organization_id', $request->user()->organization_id)
            ->whereBetween('scheduled_start_at', [$from, $to])
            ->whereIn('status', ['cancelled', 'no_show']);

        $this->applyDoctorLocationFilters($query, $request);

        return $this->success($query->groupBy('doctor_id', 'location_id', 'status')->get());
    }

    public function assistantActivity(Request $request)
    {
        [$from, $to] = $this->range($request);

        $rows = AuditLog::query()
            ->selectRaw('user_id, event, COUNT(*) total')
            ->where('organization_id', $request->user()->organization_id)
            ->whereBetween('created_at', [$from, $to])
            ->whereIn('event', [
                'patient.created', 'patient.updated', 'appointment.created',
                'appointment.rescheduled', 'appointment.cancelled', 'token.assigned',
                'token.changed', 'queue.status_changed', 'payment.created',
            ])
            ->whereHas('user', fn ($q) => $q->where('role', 'assistant'))
            ->groupBy('user_id', 'event')
            ->with('user:id,name,email')
            ->get();

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

    public function notifications(Request $request)
    {
        [$from, $to] = $this->range($request);

        $rows = NotificationLog::query()
            ->selectRaw('channel, type, status, COUNT(*) total, SUM(attempts) total_attempts')
            ->where('organization_id', $request->user()->organization_id)
            ->whereBetween('created_at', [$from, $to])
            ->groupBy('channel', 'type', 'status')
            ->get();

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

    public function export(Request $request)
    {
        $validated = $request->validate([
            'report' => ['required', 'in:appointments,revenue,cancellations'],
            'date_from' => ['nullable', 'date'],
            'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
            'doctor_id' => ['nullable', 'integer'],
            'location_id' => ['nullable', 'integer'],
        ]);

        $rows = match ($validated['report']) {
            'appointments' => $this->appointmentRows($request),
            'revenue' => $this->revenueRows($request),
            'cancellations' => $this->cancellationRows($request),
        };

        $filename = 'smart-clinic-'.$validated['report'].'-'.now()->format('Ymd-His').'.csv';

        return response()->streamDownload(function () use ($rows) {
            $handle = fopen('php://output', 'w');
            if ($rows->isEmpty()) {
                fputcsv($handle, ['no_data']);
                fclose($handle);

                return;
            }

            $normalized = $rows->map(fn ($row) => $row instanceof Arrayable ? $row->toArray() : (array) $row);
            fputcsv($handle, array_keys($normalized->first()));
            foreach ($normalized as $row) {
                fputcsv($handle, array_map(fn ($value) => is_scalar($value) || $value === null ? $value : json_encode($value), $row));
            }
            fclose($handle);
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    private function cancellationRows(Request $request): Collection
    {
        [$from, $to] = $this->range($request);
        $query = Appointment::query()
            ->selectRaw('doctor_id, location_id, status, COUNT(*) total')
            ->where('organization_id', $request->user()->organization_id)
            ->whereBetween('scheduled_start_at', [$from, $to])
            ->whereIn('status', ['cancelled', 'no_show']);

        $this->applyDoctorLocationFilters($query, $request);

        return $query->groupBy('doctor_id', 'location_id', 'status')->get();
    }

    private function applyDoctorLocationFilters($query, Request $request): void
    {
        if ($request->filled('doctor_id')) {
            $query->where('doctor_id', $request->integer('doctor_id'));
        }
        if ($request->filled('location_id')) {
            $query->where('location_id', $request->integer('location_id'));
        }
    }
}
