<?php

namespace App\Domain\Support\Services;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Audit\Enums\ActivityCategory;
use App\Domain\Audit\Services\ActivityLogger;
use App\Domain\Communication\Enums\TicketCategory;
use App\Domain\Communication\Enums\TicketStatus;
use App\Domain\Communication\Services\Notifier;
use App\Domain\Identity\Models\User;
use App\Domain\Support\Models\SupportTicket;
use App\Domain\Support\Models\SupportTicketMessage;
use App\Support\Sequences\SequenceGenerator;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * Support tickets (§59).
 *
 * The SLA is set from the category rather than typed in, and the first staff
 * reply is what stops the clock — not a status change, which costs nothing.
 */
class TicketService
{
    public function __construct(
        private readonly SequenceGenerator $sequences,
        private readonly ActivityLogger $activity,
        private readonly Notifier $notifier,
    ) {}

    /**
     * @param  array<string, mixed>  $attributes
     *
     * @throws ValidationException
     */
    public function open(
        Applicant $applicant,
        string $subject,
        TicketCategory $category,
        string $body,
        ?User $actor,
        array $attributes = [],
    ): SupportTicket {
        if (blank($subject) || blank($body)) {
            throw ValidationException::withMessages([
                'subject' => ['A ticket needs a subject and a description.'],
            ]);
        }

        return DB::transaction(function () use ($applicant, $subject, $category, $body, $actor, $attributes) {
            $ticket = SupportTicket::create([
                'ticket_no' => $this->nextNumber($applicant->company_id),
                'company_id' => $applicant->company_id,
                'branch_id' => $applicant->branch_id,
                'applicant_id' => $applicant->getKey(),
                'subject' => $subject,
                'category' => $category->value,
                'priority' => $attributes['priority'] ?? 'normal',
                'status' => TicketStatus::Open->value,
                // The clock comes from what the ticket is about, not from a form.
                'due_at' => now()->addHours($category->slaHours()),
                'opened_by' => $actor?->getKey(),
                'opened_at' => now(),
            ]);

            SupportTicketMessage::create([
                'support_ticket_id' => $ticket->getKey(),
                'author_id' => $actor?->getKey(),
                'author_name' => $actor?->name ?? $applicant->full_name,
                'author_side' => $attributes['author_side'] ?? 'applicant',
                'body' => $body,
                'is_internal' => false,
            ]);

            $this->activity->log(ActivityCategory::Communication, 'ticket.opened', [
                'subject' => $applicant,
                'actor' => $actor,
                'title' => 'Support ticket opened',
                'description' => $ticket->ticket_no.' · '.$subject,
                'visibility' => 'shared',
            ]);

            $this->notifier->send('ticket_opened', $applicant, [
                'applicant_name' => $applicant->full_name,
                'ticket_no' => $ticket->ticket_no,
                'subject' => $subject,
            ], $ticket, $actor);

            return $ticket->fresh();
        });
    }

    /**
     * @throws ValidationException
     */
    public function reply(
        SupportTicket $ticket,
        string $body,
        User $actor,
        bool $internal = false,
        string $side = 'staff',
    ): SupportTicketMessage {
        if ($ticket->status->isClosed()) {
            throw ValidationException::withMessages([
                'ticket' => ["This ticket is {$ticket->status->label()}; reopen it before replying."],
            ]);
        }

        if (blank($body)) {
            throw ValidationException::withMessages(['body' => ['Write something to send.']]);
        }

        return DB::transaction(function () use ($ticket, $body, $actor, $internal, $side) {
            $message = SupportTicketMessage::create([
                'support_ticket_id' => $ticket->getKey(),
                'author_id' => $actor->getKey(),
                'author_name' => $actor->name,
                'author_side' => $side,
                'body' => $body,
                'is_internal' => $internal,
            ]);

            // Only a real reply to the applicant stops the SLA clock. An
            // internal note is the agency talking to itself.
            if ($side === 'staff' && ! $internal && $ticket->first_responded_at === null) {
                $ticket->first_responded_at = now();
            }

            $ticket->status = match (true) {
                $side === 'applicant' => TicketStatus::InProgress,
                $internal => $ticket->status,
                default => TicketStatus::WaitingApplicant,
            };

            $ticket->save();

            if ($side === 'staff' && ! $internal && $ticket->applicant !== null) {
                $this->notifier->send('ticket_reply', $ticket->applicant, [
                    'applicant_name' => $ticket->applicant->full_name,
                    'ticket_no' => $ticket->ticket_no,
                    'subject' => $ticket->subject,
                ], $ticket, $actor);
            }

            return $message;
        });
    }

    /** @throws ValidationException */
    public function assign(SupportTicket $ticket, User $assignee, User $actor): SupportTicket
    {
        if ($ticket->status->isClosed()) {
            throw ValidationException::withMessages([
                'ticket' => ['A closed ticket cannot be reassigned.'],
            ]);
        }

        $ticket->assigned_to = $assignee->getKey();

        if ($ticket->status === TicketStatus::Open) {
            $ticket->status = TicketStatus::InProgress;
        }

        $ticket->save();

        return $ticket;
    }

    /** @throws ValidationException */
    public function changeStatus(SupportTicket $ticket, TicketStatus $target, User $actor, ?string $note = null): SupportTicket
    {
        if ($target === TicketStatus::Resolved && blank($note)) {
            throw ValidationException::withMessages([
                'resolution' => ['Say how the ticket was resolved.'],
            ]);
        }

        return DB::transaction(function () use ($ticket, $target, $actor, $note) {
            $previous = $ticket->status;

            $ticket->status = $target;

            if ($target === TicketStatus::Resolved) {
                $ticket->resolved_at = now();
                $ticket->resolution = $note;
            }

            $ticket->save();

            $this->activity->log(ActivityCategory::Communication, 'ticket.status_changed', [
                'subject' => $ticket->applicant,
                'actor' => $actor,
                'title' => 'Support ticket '.strtolower($target->label()),
                'description' => $ticket->ticket_no.' · '.$previous->label().' → '.$target->label(),
                'visibility' => 'shared',
            ]);

            if ($target === TicketStatus::Resolved && $ticket->applicant !== null) {
                $this->notifier->send('ticket_resolved', $ticket->applicant, [
                    'applicant_name' => $ticket->applicant->full_name,
                    'ticket_no' => $ticket->ticket_no,
                    'resolution' => $note ?? '',
                ], $ticket, $actor);
            }

            return $ticket;
        });
    }

    private function nextNumber(int $companyId): string
    {
        return $this->sequences->nextUnique(
            'support_ticket',
            'TKT',
            [now()->year],
            6,
            fn (string $no) => SupportTicket::where('company_id', $companyId)
                ->where('ticket_no', $no)
                ->exists(),
        );
    }
}
