<?php

namespace App\Services\Dashboard;

use App\Models\Client\Client;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;

class LeadPerformanceMetrics
{
    public function __construct(
        private readonly int $solicitorId,
        private readonly ?int $leadSourceId = null,
    ) {}

    public function report(
        Carbon $from,
        Carbon $to,
        ?Carbon $heatmapFrom = null,
        ?Carbon $heatmapTo = null,
    ): array
    {
        $leadQuery = $this->leadQuery($from, $to);
        $leads = (clone $leadQuery)
            ->get(['clients.id', 'clients.created_at', 'clients.first_successful_contact_at']);
        $total = $leads->count();

        $contacted = $leads->whereNotNull('first_successful_contact_at')->count();
        $sentIds = $this->contractClientIds($from, $to);
        $convertedRows = $this->convertedRows($from, $to);
        $convertedIds = $convertedRows->pluck('client_id')->map(fn ($id) => (int) $id);

        $conversionDurations = $convertedRows
            ->map(function ($row) {
                $receivedAt = Carbon::parse($row->lead_received_at);
                $convertedAt = Carbon::parse($row->converted_at);

                return max(0, $receivedAt->diffInSeconds($convertedAt, false));
            })
            ->sort()
            ->values();

        $closedIds = $this->closedClientIds($from, $to);
        $statusCounts = [
            'New' => 0,
            'Contacted' => 0,
            'Engaged' => 0,
            'Qualified / Converted' => 0,
            'Lost / Unqualified' => 0,
        ];

        $sentLookup = array_fill_keys($sentIds->all(), true);
        $convertedLookup = array_fill_keys($convertedIds->all(), true);
        $closedLookup = array_fill_keys($closedIds->all(), true);

        foreach ($leads as $lead) {
            $status = match (true) {
                isset($closedLookup[$lead->id]) => 'Lost / Unqualified',
                isset($convertedLookup[$lead->id]) => 'Qualified / Converted',
                isset($sentLookup[$lead->id]) => 'Engaged',
                $lead->first_successful_contact_at !== null => 'Contacted',
                default => 'New',
            };

            $statusCounts[$status]++;
        }

        $statusColours = [
            'New' => '#77a9f7',
            'Contacted' => '#2769df',
            'Engaged' => '#f1ad36',
            'Qualified / Converted' => '#20a276',
            'Lost / Unqualified' => '#dc5362',
        ];

        $statuses = collect($statusCounts)->map(fn ($count, $label) => [
            'label' => $label,
            'count' => $count,
            'percentage' => $total > 0 ? round($count / $total * 100, 1) : 0,
            'colour' => $statusColours[$label],
        ])->values()->all();

        [$volumeLabels, $volumeValues] = $this->leadVolume($from, $to);

        $heatmapLeads = $leads;
        if ($heatmapFrom && $heatmapTo) {
            $heatmapLeads = $leads->filter(function ($lead) use ($heatmapFrom, $heatmapTo) {
                return Carbon::parse($lead->created_at)->betweenIncluded($heatmapFrom, $heatmapTo);
            });
        }

        [$heatmap, $heatmapMax] = $this->heatmap($heatmapLeads);

        $medianSeconds = $this->median($conversionDurations->all());

        return [
            'periodLabel' => $from->isSameDay($to)
                ? $from->format('j F Y')
                : $from->format('j M Y').' – '.$to->format('j M Y'),
            'newLeads' => $total,
            'contactRate' => $total > 0 ? round($contacted / $total * 100, 1) : null,
            'conversionRate' => $total > 0 ? round($convertedIds->count() / $total * 100, 1) : null,
            'medianConversionSeconds' => $medianSeconds,
            'medianConversionLabel' => $this->durationLabel($medianSeconds),
            'leadVolume' => [
                'labels' => $volumeLabels,
                'values' => $volumeValues,
            ],
            'heatmap' => $heatmap,
            'heatmapMax' => $heatmapMax,
            'statuses' => $statuses,
            'statusTotal' => $total,
        ];
    }

    private function leadQuery(Carbon $from, Carbon $to)
    {
        return Client::query()
            ->where('clients.solicitor_id', $this->solicitorId)
            ->when($this->leadSourceId, fn ($query, $id) => $query->where('clients.lead_source_id', $id))
            ->whereBetween('clients.created_at', [$from, $to]);
    }

    private function contractClientIds(Carbon $from, Carbon $to)
    {
        return DB::table('contract_requests')
            ->join('esign_documents', 'esign_documents.contract_request_id', '=', 'contract_requests.id')
            ->join('clients', 'clients.id', '=', 'contract_requests.client_id')
            ->where('clients.solicitor_id', $this->solicitorId)
            ->when($this->leadSourceId, fn ($query, $id) => $query->where('clients.lead_source_id', $id))
            ->whereNull('clients.deleted_at')
            ->whereBetween('clients.created_at', [$from, $to])
            ->whereNotNull('esign_documents.sent_at')
            ->distinct()
            ->pluck('contract_requests.client_id')
            ->map(fn ($id) => (int) $id);
    }

    private function convertedRows(Carbon $from, Carbon $to)
    {
        $documents = DB::table('contract_requests')
            ->join('esign_documents', 'esign_documents.contract_request_id', '=', 'contract_requests.id')
            ->join('clients', 'clients.id', '=', 'contract_requests.client_id')
            ->where('clients.solicitor_id', $this->solicitorId)
            ->when($this->leadSourceId, fn ($query, $id) => $query->where('clients.lead_source_id', $id))
            ->whereNull('clients.deleted_at')
            ->whereBetween('clients.created_at', [$from, $to])
            ->select([
                'contract_requests.client_id',
                'contract_requests.id as request_id',
                'clients.created_at as lead_received_at',
                'esign_documents.sent_at',
                'esign_documents.signed_at',
                'esign_documents.completed_at',
            ])
            ->get();

        return $documents
            ->groupBy('client_id')
            ->map(function ($clientDocuments, $clientId) {
                $completedAt = $clientDocuments
                    ->groupBy('request_id')
                    ->map(function ($requestDocuments) {
                        $sentDocuments = $requestDocuments->whereNotNull('sent_at');
                        if ($sentDocuments->isEmpty()
                            || ! $sentDocuments->every(
                                fn ($document) => $document->signed_at || $document->completed_at
                            )) {
                            return null;
                        }

                        return $sentDocuments
                            ->map(fn ($document) => Carbon::parse(
                                $document->completed_at ?? $document->signed_at
                            ))
                            ->max();
                    })
                    ->filter()
                    ->min();

                if (! $completedAt) {
                    return null;
                }

                return (object) [
                    'client_id' => (int) $clientId,
                    'lead_received_at' => $clientDocuments->first()->lead_received_at,
                    'converted_at' => $completedAt,
                ];
            })
            ->filter()
            ->values();
    }

    private function closedClientIds(Carbon $from, Carbon $to)
    {
        return DB::table('clients')
            ->join('claims', 'claims.client_id', '=', 'clients.id')
            ->join('statuses', 'statuses.id', '=', 'claims.status_id')
            ->where('clients.solicitor_id', $this->solicitorId)
            ->when($this->leadSourceId, fn ($query, $id) => $query->where('clients.lead_source_id', $id))
            ->whereNull('clients.deleted_at')
            ->whereNull('claims.deleted_at')
            ->whereBetween('clients.created_at', [$from, $to])
            ->whereRaw('LOWER(statuses.display_name) = ?', ['closed'])
            ->distinct()
            ->pluck('clients.id')
            ->map(fn ($id) => (int) $id);
    }

    private function leadVolume(Carbon $from, Carbon $to): array
    {
        $counts = $this->leadQuery($from, $to)
            ->selectRaw('DATE(clients.created_at) as lead_date, COUNT(*) as aggregate')
            ->groupBy('lead_date')
            ->pluck('aggregate', 'lead_date');

        $labels = [];
        $values = [];
        for ($date = $from->copy()->startOfDay(); $date->lte($to); $date->addDay()) {
            $key = $date->toDateString();
            $labels[] = $date->format('d M');
            $values[] = (int) ($counts[$key] ?? 0);
        }

        return [$labels, $values];
    }

    private function heatmap($leads): array
    {
        $matrix = array_fill(0, 24, array_fill(0, 7, 0));
        $max = 0;

        foreach ($leads as $lead) {
            $receivedAt = Carbon::parse($lead->created_at);
            $hour = $receivedAt->hour;
            $day = $receivedAt->dayOfWeekIso - 1;
            $matrix[$hour][$day]++;
            $max = max($max, $matrix[$hour][$day]);
        }

        return [$matrix, $max];
    }

    private function median(array $values): ?float
    {
        $count = count($values);
        if ($count === 0) {
            return null;
        }

        $middle = intdiv($count, 2);

        return $count % 2
            ? (float) $values[$middle]
            : ($values[$middle - 1] + $values[$middle]) / 2;
    }

    private function durationLabel(?float $seconds): string
    {
        if ($seconds === null) {
            return 'No conversions';
        }

        $hours = $seconds / 3600;
        if ($hours < 24) {
            return number_format($hours, 1).' hours';
        }

        return number_format($hours / 24, 1).' days';
    }
}
