<?php

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

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Communication\Enums\TicketCategory;
use App\Domain\Communication\Enums\TicketStatus;
use App\Domain\Identity\Models\User;
use App\Domain\Support\Models\SupportTicket;
use App\Domain\Support\Models\SupportTicketMessage;
use App\Domain\Support\Services\TicketService;
use App\Http\Controllers\Controller;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

/** Support and complaints (§59). */
class SupportTicketController extends Controller
{
    public function __construct(private readonly TicketService $tickets) {}

    public function index(Request $request): JsonResponse
    {
        abort_unless($request->user()->can('support.view'), 403);

        $tickets = SupportTicket::query()
            ->with(['applicant:id,uuid,full_name,reference_no', 'assignee:id,name'])
            ->where('company_id', $request->user()->company_id)
            ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status')))
            ->when($request->filled('category'), fn ($q) => $q->where('category', $request->string('category')))
            ->when($request->boolean('open_only'), fn ($q) => $q->open())
            ->when($request->boolean('overdue'), fn ($q) => $q->overdue())
            ->when($request->boolean('mine'), fn ($q) => $q->where('assigned_to', $request->user()->getKey()))
            ->latest('opened_at')
            ->latest('id')
            ->paginate($request->integer('per_page', 25));

        return ApiResponse::success(
            $tickets->through(fn (SupportTicket $ticket) => $this->present($ticket)),
            meta: [
                'statuses' => array_map(
                    fn (TicketStatus $status) => ['value' => $status->value, 'label' => $status->label()],
                    TicketStatus::cases(),
                ),
                'categories' => array_map(
                    fn (TicketCategory $category) => [
                        'value' => $category->value,
                        'label' => $category->label(),
                        'sla_hours' => $category->slaHours(),
                    ],
                    TicketCategory::cases(),
                ),
            ],
        );
    }

    public function show(Request $request, SupportTicket $ticket): JsonResponse
    {
        $this->assertMayRead($request, $ticket);

        $ticket->load(['applicant:id,uuid,full_name,reference_no', 'assignee:id,name']);

        // An applicant reads their thread without the agency's internal notes.
        $messages = $this->isApplicant($request)
            ? $ticket->visibleMessages()->get()
            : $ticket->messages()->get();

        return ApiResponse::success(array_merge($this->present($ticket), [
            'resolution' => $ticket->resolution,
            'messages' => $messages->map(fn (SupportTicketMessage $message) => [
                'id' => $message->uuid,
                'author' => $message->author_name,
                'side' => $message->author_side,
                'body' => $message->body,
                'is_internal' => $message->is_internal,
                'created_at' => $message->created_at?->toIso8601String(),
            ]),
        ]));
    }

    public function store(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'applicant_id' => ['nullable', 'uuid'],
            'subject' => ['required', 'string', 'max:255'],
            'category' => ['required', Rule::in(TicketCategory::values())],
            'body' => ['required', 'string', 'max:5000'],
            'priority' => ['nullable', Rule::in(['low', 'normal', 'high', 'urgent'])],
        ]);

        $applicant = $this->resolveApplicant($request, $validated['applicant_id'] ?? null);

        $ticket = $this->tickets->open(
            $applicant,
            $validated['subject'],
            TicketCategory::from($validated['category']),
            $validated['body'],
            $request->user(),
            array_merge($validated, [
                'author_side' => $this->isApplicant($request) ? 'applicant' : 'staff',
            ]),
        );

        return ApiResponse::success(
            $this->present($ticket->fresh('applicant')),
            "Logged as {$ticket->ticket_no}.",
            status: 201,
        );
    }

    public function reply(Request $request, SupportTicket $ticket): JsonResponse
    {
        $this->assertMayRead($request, $ticket);

        $isApplicant = $this->isApplicant($request);

        abort_unless($isApplicant || $request->user()->can('support.respond'), 403);

        $validated = $request->validate([
            'body' => ['required', 'string', 'max:5000'],
            'is_internal' => ['nullable', 'boolean'],
        ]);

        // An applicant cannot write an internal note, whatever they post.
        $internal = ! $isApplicant && (bool) ($validated['is_internal'] ?? false);

        $this->tickets->reply(
            $ticket,
            $validated['body'],
            $request->user(),
            $internal,
            $isApplicant ? 'applicant' : 'staff',
        );

        return ApiResponse::success($this->present($ticket->fresh('applicant')), 'Reply sent.');
    }

    public function assign(Request $request, SupportTicket $ticket): JsonResponse
    {
        abort_unless($request->user()->can('support.assign'), 403);
        $this->assertSameCompany($request, $ticket);

        $validated = $request->validate(['user_id' => ['required', 'uuid']]);

        $assignee = User::where('uuid', $validated['user_id'])
            ->where('company_id', $request->user()->company_id)
            ->firstOrFail();

        return ApiResponse::success(
            $this->present($this->tickets->assign($ticket, $assignee, $request->user())->fresh('assignee')),
            "Assigned to {$assignee->name}.",
        );
    }

    public function changeStatus(Request $request, SupportTicket $ticket): JsonResponse
    {
        abort_unless($request->user()->can('support.respond'), 403);
        $this->assertSameCompany($request, $ticket);

        $validated = $request->validate([
            'status' => ['required', Rule::in(TicketStatus::values())],
            'note' => ['nullable', 'string', 'max:2000'],
        ]);

        $target = TicketStatus::from($validated['status']);

        abort_unless(
            ! $target->isClosed() || $request->user()->can('support.close'),
            403,
        );

        $updated = $this->tickets->changeStatus($ticket, $target, $request->user(), $validated['note'] ?? null);

        return ApiResponse::success($this->present($updated->fresh('applicant')), "Now {$target->label()}.");
    }

    private function isApplicant(Request $request): bool
    {
        return $request->user()->user_type === 'applicant';
    }

    private function resolveApplicant(Request $request, ?string $uuid): Applicant
    {
        if ($this->isApplicant($request)) {
            $applicant = Applicant::where('user_id', $request->user()->getKey())->first();

            abort_if($applicant === null, 403, 'No applicant record is linked to this account.');

            return $applicant;
        }

        abort_unless($request->user()->can('support.create'), 403);
        abort_if($uuid === null, 422, 'Say which applicant this is about.');

        return Applicant::where('uuid', $uuid)
            ->where('company_id', $request->user()->company_id)
            ->firstOrFail();
    }

    private function assertMayRead(Request $request, SupportTicket $ticket): void
    {
        if ($this->isApplicant($request)) {
            $applicant = Applicant::where('user_id', $request->user()->getKey())->first();

            abort_unless($applicant !== null && $ticket->applicant_id === $applicant->getKey(), 403);

            return;
        }

        abort_unless($request->user()->can('support.view'), 403);
        $this->assertSameCompany($request, $ticket);
    }

    private function assertSameCompany(Request $request, SupportTicket $ticket): void
    {
        abort_unless($ticket->company_id === $request->user()->company_id, 403);
    }

    /** @return array<string, mixed> */
    private function present(SupportTicket $ticket): array
    {
        return [
            'id' => $ticket->uuid,
            'ticket_no' => $ticket->ticket_no,
            'subject' => $ticket->subject,
            'category' => $ticket->category->value,
            'category_label' => $ticket->category->label(),
            'priority' => $ticket->priority,
            'status' => $ticket->status->value,
            'status_label' => $ticket->status->label(),
            'is_closed' => $ticket->status->isClosed(),
            'is_overdue' => $ticket->isOverdue(),
            'due_at' => $ticket->due_at?->toIso8601String(),
            'first_responded_at' => $ticket->first_responded_at?->toIso8601String(),
            'resolved_at' => $ticket->resolved_at?->toIso8601String(),
            'opened_at' => $ticket->opened_at?->toIso8601String(),
            'assignee' => $ticket->relationLoaded('assignee') ? $ticket->assignee?->name : null,
            'applicant' => $ticket->relationLoaded('applicant') && $ticket->applicant ? [
                'id' => $ticket->applicant->uuid,
                'name' => $ticket->applicant->full_name,
                'reference' => $ticket->applicant->reference_no,
            ] : null,
        ];
    }
}
