<?php

namespace App\Domain\Workflow\Services;

use App\Domain\Audit\Enums\ActivityCategory;
use App\Domain\Audit\Services\ActivityLogger;
use App\Domain\Identity\Models\User;
use App\Domain\Workflow\Enums\AppointmentStatus;
use App\Domain\Workflow\Models\Appointment;
use App\Support\Sequences\SequenceGenerator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class AppointmentService
{
    public function __construct(
        private readonly SequenceGenerator $sequences,
        private readonly ActivityLogger $activity,
    ) {}

    /** @param array<string, mixed> $attributes */
    public function schedule(array $attributes, ?Model $subject, User $actor): Appointment
    {
        return DB::transaction(function () use ($attributes, $subject, $actor) {
            $appointment = Appointment::create(array_merge($attributes, [
                'appointment_no' => $this->sequences->nextUnique(
                    'appointment',
                    'APT',
                    [now()->year],
                    5,
                    fn (string $no) => Appointment::where('company_id', $actor->company_id)
                        ->where('appointment_no', $no)
                        ->exists(),
                ),
                'company_id' => $actor->company_id,
                'branch_id' => $attributes['branch_id'] ?? $actor->branch_id,
                'subject_type' => $subject?->getMorphClass(),
                'subject_id' => $subject?->getKey(),
                'status' => AppointmentStatus::Scheduled->value,
                'created_by' => $actor->getKey(),
            ]));

            $this->activity->log(ActivityCategory::Task, 'appointment.scheduled', [
                'subject' => $subject ?? $appointment,
                'actor' => $actor,
                'title' => 'Appointment scheduled',
                'description' => $appointment->title.' · '.$appointment->scheduled_at->format('j M Y, g:i a'),
                // The applicant is told about their own appointments.
                'visibility' => $appointment->is_visible_to_applicant ? 'shared' : 'internal',
            ]);

            return $appointment;
        });
    }

    /**
     * Moves an appointment to a new time by creating a replacement, leaving the
     * original marked Rescheduled (§20 "reschedule history").
     *
     * @throws ValidationException
     */
    public function reschedule(Appointment $appointment, Carbon $to, string $reason, User $actor): Appointment
    {
        if ($appointment->status->isClosed()) {
            throw ValidationException::withMessages([
                'appointment' => ["A {$appointment->status->label()} appointment cannot be rescheduled."],
            ]);
        }

        if (blank($reason)) {
            throw ValidationException::withMessages([
                'reason' => ['Say why the appointment is moving.'],
            ]);
        }

        return DB::transaction(function () use ($appointment, $to, $reason, $actor) {
            $replacement = $appointment->replicate([
                'uuid', 'appointment_no', 'status', 'attended_at', 'outcome', 'outcome_notes',
                'rescheduled_from_id', 'reschedule_reason',
            ]);

            $replacement->appointment_no = $this->sequences->nextUnique(
                'appointment',
                'APT',
                [now()->year],
                5,
                fn (string $no) => Appointment::where('company_id', $appointment->company_id)
                    ->where('appointment_no', $no)
                    ->exists(),
            );
            $replacement->scheduled_at = $to;
            $replacement->status = AppointmentStatus::Scheduled->value;
            $replacement->rescheduled_from_id = $appointment->getKey();
            $replacement->reschedule_reason = $reason;
            $replacement->created_by = $actor->getKey();
            $replacement->save();

            // The original is kept, not overwritten: the history of a moved
            // medical or embassy date is often what a dispute turns on.
            $appointment->status = AppointmentStatus::Rescheduled;
            $appointment->save();

            $this->activity->log(ActivityCategory::Task, 'appointment.rescheduled', [
                'subject' => $appointment->subject ?? $replacement,
                'actor' => $actor,
                'title' => 'Appointment rescheduled',
                'description' => $appointment->scheduled_at->format('j M Y, g:i a').
                    ' → '.$to->format('j M Y, g:i a'),
                'metadata' => ['reason' => $reason],
                'visibility' => $replacement->is_visible_to_applicant ? 'shared' : 'internal',
            ]);

            return $replacement;
        });
    }

    /** @throws ValidationException */
    public function recordOutcome(
        Appointment $appointment,
        AppointmentStatus $status,
        ?string $outcome,
        ?string $notes,
        User $actor,
    ): Appointment {
        if (! in_array($status, [
            AppointmentStatus::Attended,
            AppointmentStatus::NoShow,
            AppointmentStatus::Cancelled,
            AppointmentStatus::Confirmed,
        ], true)) {
            throw ValidationException::withMessages([
                'status' => ['That is not an outcome an appointment can be given.'],
            ]);
        }

        if ($status === AppointmentStatus::Cancelled && blank($notes)) {
            throw ValidationException::withMessages([
                'notes' => ['Say why the appointment was cancelled.'],
            ]);
        }

        return DB::transaction(function () use ($appointment, $status, $outcome, $notes, $actor) {
            $appointment->status = $status;
            $appointment->outcome = $outcome;
            $appointment->outcome_notes = $notes;

            if ($status === AppointmentStatus::Attended) {
                $appointment->attended_at = now();
            }

            $appointment->save();

            $this->activity->log(ActivityCategory::Task, 'appointment.outcome_recorded', [
                'subject' => $appointment->subject ?? $appointment,
                'actor' => $actor,
                'title' => 'Appointment '.strtolower($status->label()),
                'description' => $appointment->title.($outcome ? " · {$outcome}" : ''),
                'visibility' => $appointment->is_visible_to_applicant ? 'shared' : 'internal',
            ]);

            return $appointment;
        });
    }
}
