<?php

namespace App\Domain\Applicants\Services;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Billing\Enums\InvoiceStatus;
use App\Domain\Billing\Models\Invoice;
use App\Domain\Documents\Services\ChecklistBuilder;
use App\Domain\Payments\Enums\PaymentStatus;
use App\Domain\Payments\Models\Payment;
use App\Domain\Refunds\Enums\RefundStatus;
use App\Domain\Refunds\Models\Refund;
use App\Domain\Training\Models\TrainingParticipant;
use App\Domain\Workflow\Models\Appointment;
use App\Domain\Workflow\Models\PipelineStage;
use Illuminate\Support\Collection;

/**
 * Builds everything the applicant portal shows (§26).
 *
 * This class is the single boundary between internal data and what an applicant
 * may see. Nothing here reads internal notes, staff commentary, employer
 * commercials, agent commission or costs — and the tests assert that.
 */
class ApplicantPortalPresenter
{
    public function __construct(private readonly ChecklistBuilder $checklists) {}

    /** @return array<string, mixed> */
    public function dashboard(Applicant $applicant): array
    {
        $checklist = $this->checklists->forApplicant($applicant);

        return [
            'applicant' => [
                'reference_no' => $applicant->reference_no,
                'full_name' => $applicant->full_name,
                'photo_url' => $applicant->photo_path ? url('storage/'.$applicant->photo_path) : null,
                'destination_country' => $applicant->destination_country,
                // The job title only; employer commercials stay internal.
                'job_title' => $applicant->jobDemand?->job_title,
                'is_verified' => $applicant->verified_at !== null,
            ],
            'status' => $this->currentStatus($applicant),
            'timeline' => $this->timeline($applicant),
            'next_actions' => $this->nextActions($checklist['items']),
            'documents_summary' => [
                'completion_percent' => $checklist['summary']['completion_percent'],
                'verified' => $checklist['summary']['verified'],
                'mandatory' => $checklist['summary']['mandatory'],
                'needs_action' => $checklist['summary']['needs_action'],
            ],
            'appointments' => $this->appointments($applicant),
            'training' => $this->training($applicant),
            'payments' => $this->payments($applicant),
            'messages' => [],
        ];
    }

    /**
     * Training the applicant is booked on (§22).
     *
     * The public label only: an internal "failed" reads as something to be
     * repeated, and the score stays internal.
     *
     * @return array<int, array<string, mixed>>
     */
    private function training(Applicant $applicant): array
    {
        return TrainingParticipant::where('applicant_id', $applicant->getKey())
            ->with('program')
            ->get()
            ->sortByDesc(fn (TrainingParticipant $participant) => $participant->program->starts_on)
            ->map(fn (TrainingParticipant $participant) => [
                'title' => $participant->program->title,
                'location' => $participant->program->location,
                'starts_on' => $participant->program->starts_on->toDateString(),
                'ends_on' => $participant->program->ends_on?->toDateString(),
                'status_label' => $participant->status->publicLabel(),
                'certificate_issued' => (bool) $participant->certificate_issued,
                'certificate_no' => $participant->certificate_no,
            ])
            ->values()
            ->all();
    }

    /**
     * What the applicant is allowed to see about their own money (§26, §58).
     *
     * Issued invoices only — a draft is internal thinking. Line-level internal
     * cost, agent commission and margin are never read here at all, so they
     * cannot leak through a later edit to this method.
     *
     * @return array<string, mixed>|null
     */
    private function payments(Applicant $applicant): ?array
    {
        $invoices = Invoice::where('applicant_id', $applicant->getKey())
            ->whereIn('status', [
                InvoiceStatus::Issued->value,
                InvoiceStatus::PartiallyPaid->value,
                InvoiceStatus::Paid->value,
            ])
            ->with(['lines', 'installments'])
            ->orderBy('id')
            ->get();

        if ($invoices->isEmpty()) {
            return null;
        }

        $currency = $invoices->first()->currency;

        $receipts = Payment::where('applicant_id', $applicant->getKey())
            ->where('status', PaymentStatus::Confirmed->value)
            ->whereNull('reverses_payment_id')
            ->orderByDesc('received_at')
            ->get();

        $refunds = Refund::where('applicant_id', $applicant->getKey())
            ->whereNotIn('status', [RefundStatus::Cancelled->value])
            ->orderByDesc('requested_at')
            ->get();

        return [
            'currency' => $currency,
            'total_amount' => (int) $invoices->sum('total_amount'),
            'paid_amount' => (int) $invoices->sum('paid_amount'),
            'outstanding_amount' => max(0, (int) $invoices->sum('total_amount') - (int) $invoices->sum('paid_amount')),
            'invoices' => $invoices->map(fn (Invoice $invoice) => [
                'invoice_no' => $invoice->invoice_no,
                'status_label' => $invoice->status->label(),
                'currency' => $invoice->currency,
                'total_amount' => (int) $invoice->total_amount,
                'paid_amount' => (int) $invoice->paid_amount,
                'outstanding_amount' => $invoice->outstanding()->minorUnits,
                'due_on' => $invoice->due_on?->toDateString(),
                'lines' => $invoice->lines->map(fn ($line) => [
                    'description' => $line->description,
                    'quantity' => (int) $line->quantity,
                    'line_total' => (int) $line->line_total,
                ])->all(),
                'installments' => $invoice->installments->map(fn ($installment) => [
                    'label' => $installment->label,
                    'due_on' => $installment->due_on?->toDateString(),
                    'amount' => (int) $installment->amount,
                    'paid_amount' => (int) $installment->paid_amount,
                    'status_label' => $installment->status->label(),
                ])->all(),
            ])->all(),
            'receipts' => $receipts->map(fn (Payment $payment) => [
                'receipt_no' => $payment->receipt_no,
                'amount' => (int) $payment->amount,
                'currency' => $payment->currency,
                'method_label' => $payment->method->label(),
                'received_on' => $payment->received_at?->toDateString(),
            ])->all(),
            'refunds' => $refunds->map(fn (Refund $refund) => [
                'refund_no' => $refund->refund_no,
                'status_label' => $refund->status->label(),
                'currency' => $refund->currency,
                'requested_amount' => (int) $refund->requested_amount,
                'paid_amount' => (int) $refund->paid_amount,
                'requested_on' => $refund->requested_at?->toDateString(),
                'paid_on' => $refund->paid_on?->toDateString(),
            ])->all(),
        ];
    }

    /** @return array<string, mixed> */
    public function currentStatus(Applicant $applicant): array
    {
        $stage = $applicant->currentStage;

        return [
            'label' => $stage?->publicLabel() ?? 'Application received',
            'description' => $stage?->applicant_visible_description,
            'entered_at' => $applicant->stage_entered_at?->toDateString(),
            'is_on_hold' => (bool) $stage?->is_exception,
            'is_complete' => (bool) $stage?->is_terminal,
        ];
    }

    /**
     * The §26 "My Status" timeline: the pipeline's visible stages with each
     * marked done, current or pending. Internal stage names never appear.
     *
     * @return array<int, array{label: string, description: ?string, state: string}>
     */
    public function timeline(Applicant $applicant): array
    {
        if ($applicant->pipeline_id === null) {
            return [];
        }

        $current = $applicant->currentStage;

        return $this->visibleStages($applicant)
            ->map(function (PipelineStage $stage) use ($current) {
                $state = match (true) {
                    $current === null => 'pending',
                    $stage->is($current) => 'current',
                    $stage->sequence < $current->sequence => 'done',
                    default => 'pending',
                };

                return [
                    'label' => $stage->publicLabel(),
                    'description' => $stage->applicant_visible_description,
                    'state' => $state,
                ];
            })
            ->values()
            ->all();
    }

    /**
     * What the applicant has to do next (§26 "next required action").
     *
     * @param  array<int, array<string, mixed>>  $checklistItems
     * @return array<int, array{title: string, detail: ?string, type: string}>
     */
    public function nextActions(array $checklistItems): array
    {
        return collect($checklistItems)
            ->filter(fn (array $item) => $item['needs_action'])
            ->map(fn (array $item) => [
                'title' => $item['document_type']['name'],
                // The rejection reason is written for the applicant to act on.
                'detail' => $item['document']['rejection_reason'] ?? $this->actionDetail($item['status']),
                'type' => 'document',
                'status' => $item['status'],
                'status_label' => $item['status_label'],
                'document_type_id' => $item['document_type']['id'],
            ])
            ->values()
            ->all();
    }

    /**
     * The applicant's own documents, stripped of staff-only fields (§17B.9).
     *
     * @return array<int, array<string, mixed>>
     */
    public function documents(Applicant $applicant): array
    {
        $checklist = $this->checklists->forApplicant($applicant);

        return collect($checklist['items'])
            // An applicant is not shown documents no rule asked them for; those
            // are internal records attached to their file.
            ->filter(fn (array $item) => $item['is_required'])
            ->map(fn (array $item) => [
                'document_type' => [
                    'id' => $item['document_type']['id'],
                    'name' => $item['document_type']['name'],
                    'category_label' => $item['document_type']['category_label'],
                ],
                'is_mandatory' => $item['is_mandatory'],
                'status' => $item['status'],
                'status_label' => $item['status_label'],
                'needs_action' => $item['needs_action'],
                'is_satisfied' => $item['is_satisfied'],
                'rejection_reason' => $item['document']['rejection_reason'] ?? null,
                'expiry_date' => $item['document']['expiry_date'] ?? null,
                'expiring_soon' => $item['document']['expiring_soon'] ?? false,
                'uploaded_at' => $item['document']['uploaded_at'] ?? null,
                'document_id' => $item['document']['id'] ?? null,
            ])
            ->values()
            ->all();
    }

    /**
     * The information the applicant submitted (§26 "My Application"). Identity
     * numbers are shown in full — they are the applicant's own.
     *
     * @return array<string, mixed>
     */
    public function application(Applicant $applicant): array
    {
        return [
            'reference_no' => $applicant->reference_no,
            'full_name' => $applicant->full_name,
            'father_name' => $applicant->father_name,
            'mother_name' => $applicant->mother_name,
            'gender' => $applicant->gender,
            'date_of_birth' => $applicant->date_of_birth?->toDateString(),
            'marital_status' => $applicant->marital_status,
            'national_id' => $applicant->national_id,
            'passport_number' => $applicant->passport_number,
            'passport_expiry_date' => $applicant->passport_expiry_date?->toDateString(),
            'mobile' => $applicant->mobile,
            'whatsapp' => $applicant->whatsapp,
            'email' => $applicant->email,
            'present_address' => $applicant->present_address,
            'permanent_address' => $applicant->permanent_address,
            'district' => $applicant->district,
            'emergency_contact_name' => $applicant->emergency_contact_name,
            'emergency_contact_relation' => $applicant->emergency_contact_relation,
            'emergency_contact_mobile' => $applicant->emergency_contact_mobile,
            'occupation' => $applicant->occupation,
            'desired_occupation' => $applicant->desired_occupation,
            'experience_years' => $applicant->experience_years,
            'education_level' => $applicant->education_level,
            'skills' => $applicant->skills,
            'preferred_country' => $applicant->preferred_country,
        ];
    }

    /**
     * Upcoming appointments the applicant has been told about (§20, §26).
     *
     * Internal-only appointments and staff notes never appear here.
     *
     * @return array<int, array<string, mixed>>
     */
    public function appointments(Applicant $applicant): array
    {
        return Appointment::query()
            ->where('subject_type', $applicant->getMorphClass())
            ->where('subject_id', $applicant->getKey())
            ->where('is_visible_to_applicant', true)
            ->upcoming()
            ->limit(5)
            ->get()
            ->map(fn (Appointment $appointment) => [
                'type' => $appointment->type,
                'title' => $appointment->title,
                'scheduled_at' => $appointment->scheduled_at->toIso8601String(),
                'location' => $appointment->location,
                // What to bring or do — the reason an applicant reads this at all.
                'instructions' => $appointment->instructions,
            ])
            ->all();
    }

    /** @return Collection<int, PipelineStage> */
    private function visibleStages(Applicant $applicant): Collection
    {
        $current = $applicant->currentStage;

        return PipelineStage::query()
            ->where('pipeline_id', $applicant->pipeline_id)
            ->where('is_active', true)
            ->where('is_visible_to_applicant', true)
            ->where(function ($query) use ($current) {
                // Exception stages only appear once the applicant is actually
                // in one; the rest are not part of their expected journey.
                $query->where('is_exception', false);

                if ($current?->is_exception) {
                    $query->orWhere('id', $current->getKey());
                }
            })
            ->orderBy('sequence')
            ->get();
    }

    private function actionDetail(string $status): ?string
    {
        return match ($status) {
            'not_requested', 'requested' => 'Please upload this document.',
            'expired' => 'This document has expired. Please upload a current one.',
            'reupload_required' => 'Please upload a clearer or corrected copy.',
            default => null,
        };
    }
}
