<?php

namespace App\Services\Care;

use App\Models\Consultation;
use App\Models\InvestigationAttachment;
use App\Models\InvestigationRequest;
use App\Models\InvestigationResult;
use App\Models\InvestigationResultValue;
use App\Models\InvestigationType;
use App\Models\Organization;
use App\Services\Care\Workflow\WorkflowStageCompletionService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\UploadedFile;
use InvalidArgumentException;

class InvestigationService
{
    public function __construct(
        protected CareQueueBridge $queueBridge,
        protected WorkflowStageCompletionService $workflowCompletion,
    ) {}

    public function queryForOrganization(string $ownerRef, int $organizationId): Builder
    {
        return InvestigationRequest::owned($ownerRef)->where('organization_id', $organizationId);
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    public function list(string $ownerRef, int $organizationId, array $filters = [], ?int $branchId = null): LengthAwarePaginator
    {
        $query = $this->queryForOrganization($ownerRef, $organizationId)
            ->with(['patient', 'investigationType', 'practitioner', 'branch', 'result'])
            ->orderByDesc('created_at');

        if ($branchId !== null) {
            $query->where('branch_id', $branchId);
        }

        if ($status = $filters['status'] ?? null) {
            $query->where('status', $status);
        }

        if ($patientId = $filters['patient_id'] ?? null) {
            $query->where('patient_id', $patientId);
        }

        return $query->paginate((int) ($filters['per_page'] ?? 20))->withQueryString();
    }

    /**
     * @return Collection<int, InvestigationRequest>
     */
    public function workQueue(string $ownerRef, int $branchId, ?string $status = null): Collection
    {
        $query = InvestigationRequest::owned($ownerRef)
            ->where('branch_id', $branchId)
            ->whereIn('status', InvestigationRequest::activeStatuses())
            ->with(['patient', 'investigationType', 'assignedMember', 'result'])
            ->orderByRaw("CASE priority WHEN 'urgent' THEN 0 ELSE 1 END")
            ->orderBy('created_at');

        if ($status) {
            $query->where('status', $status);
        }

        return $query->get();
    }

    /**
     * @param  array<int, int>  $typeIds
     * @return Collection<int, InvestigationRequest>
     */
    public function requestFromConsultation(
        Consultation $consultation,
        string $ownerRef,
        array $typeIds,
        ?string $clinicalNotes = null,
        string $priority = 'routine',
        ?string $actorRef = null,
    ): Collection {
        $consultation->loadMissing('visit');
        $created = new Collection;

        foreach ($typeIds as $typeId) {
            $type = InvestigationType::owned($ownerRef)->findOrFail($typeId);

            $request = InvestigationRequest::create([
                'owner_ref' => $ownerRef,
                'organization_id' => $consultation->visit->organization_id,
                'branch_id' => $consultation->visit->branch_id,
                'visit_id' => $consultation->visit_id,
                'consultation_id' => $consultation->id,
                'patient_id' => $consultation->patient_id,
                'investigation_type_id' => $type->id,
                'practitioner_id' => $consultation->practitioner_id,
                'status' => InvestigationRequest::STATUS_PENDING,
                'priority' => $priority,
                'clinical_notes' => $clinicalNotes,
                'requested_by' => $actorRef,
            ]);

            AuditLogger::record(
                $ownerRef,
                'investigation.requested',
                $consultation->visit->organization_id,
                $actorRef,
                InvestigationRequest::class,
                $request->id,
            );

            $request = $request->load(['investigationType', 'patient']);
            $organization = Organization::query()->find($request->organization_id);
            if ($organization) {
                $this->queueBridge->issueForInvestigation($organization, $request);
            }

            $created->push($request->fresh(['investigationType', 'patient']));
        }

        return $created;
    }

    public function collectSample(
        InvestigationRequest $request,
        string $ownerRef,
        ?string $barcode = null,
        ?string $actorRef = null,
    ): InvestigationRequest {
        $this->assertStatus($request, InvestigationRequest::STATUS_PENDING);

        $request->update([
            'status' => InvestigationRequest::STATUS_SAMPLE_COLLECTED,
            'sample_barcode' => $barcode ?? $this->generateBarcode($request),
            'sample_collected_at' => now(),
            'sample_collected_by' => $actorRef,
        ]);

        AuditLogger::record($ownerRef, 'investigation.sample_collected', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);

        $request = $request->fresh(['patient', 'investigationType']);
        $organization = Organization::query()->find($request->organization_id);
        if ($organization) {
            $this->queueBridge->serve($organization, $request);
        }

        return $request;
    }

    public function startProcessing(
        InvestigationRequest $request,
        string $ownerRef,
        ?int $assignedMemberId = null,
        ?string $actorRef = null,
    ): InvestigationRequest {
        $this->assertStatus($request, InvestigationRequest::STATUS_SAMPLE_COLLECTED);

        $request->update([
            'status' => InvestigationRequest::STATUS_IN_PROGRESS,
            'assigned_member_id' => $assignedMemberId,
        ]);

        InvestigationResult::firstOrCreate(
            ['investigation_request_id' => $request->id],
            ['owner_ref' => $ownerRef, 'entered_by' => $actorRef, 'status' => InvestigationResult::STATUS_DRAFT],
        );

        AuditLogger::record($ownerRef, 'investigation.in_progress', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);

        return $request->fresh(['patient', 'investigationType', 'result']);
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function enterResults(
        InvestigationRequest $request,
        string $ownerRef,
        array $data,
        ?string $actorRef = null,
    ): InvestigationResult {
        if (! in_array($request->status, [InvestigationRequest::STATUS_IN_PROGRESS, InvestigationRequest::STATUS_AWAITING_REVIEW], true)) {
            throw new InvalidArgumentException('Results can only be entered when processing.');
        }

        $result = InvestigationResult::firstOrCreate(
            ['investigation_request_id' => $request->id],
            ['owner_ref' => $ownerRef, 'entered_by' => $actorRef],
        );

        $type = $request->investigationType;
        $isAbnormal = false;

        if (! empty($data['values']) && is_array($data['values'])) {
            $result->values()->delete();
            foreach ($data['values'] as $row) {
                if (empty($row['parameter'])) {
                    continue;
                }
                $abnormal = $this->isValueAbnormal(
                    $row['value'] ?? null,
                    $row['reference_low'] ?? $type->reference_low,
                    $row['reference_high'] ?? $type->reference_high,
                );
                $isAbnormal = $isAbnormal || $abnormal;

                InvestigationResultValue::create([
                    'owner_ref' => $ownerRef,
                    'investigation_result_id' => $result->id,
                    'parameter' => $row['parameter'],
                    'value' => $row['value'] ?? null,
                    'unit' => $row['unit'] ?? $type->unit,
                    'reference_low' => $row['reference_low'] ?? $type->reference_low,
                    'reference_high' => $row['reference_high'] ?? $type->reference_high,
                    'reference_text' => $row['reference_text'] ?? $type->reference_text,
                    'is_abnormal' => $abnormal,
                ]);
            }
        } elseif (! empty($data['value'])) {
            $result->values()->delete();
            $abnormal = $this->isValueAbnormal($data['value'], $type->reference_low, $type->reference_high);
            $isAbnormal = $abnormal;
            InvestigationResultValue::create([
                'owner_ref' => $ownerRef,
                'investigation_result_id' => $result->id,
                'parameter' => $type->name,
                'value' => $data['value'],
                'unit' => $type->unit,
                'reference_low' => $type->reference_low,
                'reference_high' => $type->reference_high,
                'reference_text' => $type->reference_text,
                'is_abnormal' => $abnormal,
            ]);
        }

        $result->update([
            'result_summary' => $data['result_summary'] ?? $result->result_summary,
            'interpretation' => $data['interpretation'] ?? $result->interpretation,
            'is_abnormal' => $isAbnormal,
            'entered_by' => $actorRef ?? $result->entered_by,
            'status' => InvestigationResult::STATUS_DRAFT,
        ]);

        if (! empty($data['attachments'])) {
            $this->storeAttachments($result, $ownerRef, $data['attachments'], $actorRef);
        }

        $request->update(['status' => InvestigationRequest::STATUS_AWAITING_REVIEW]);

        AuditLogger::record($ownerRef, 'investigation.results_entered', $request->organization_id, $actorRef, InvestigationResult::class, $result->id);

        return $result->fresh(['values', 'attachments']);
    }

    public function approve(InvestigationRequest $request, string $ownerRef, ?string $actorRef = null): InvestigationRequest
    {
        $this->assertStatus($request, InvestigationRequest::STATUS_AWAITING_REVIEW);

        $result = $request->result;
        abort_unless($result, 422, 'No results to approve.');

        $result->update([
            'status' => InvestigationResult::STATUS_APPROVED,
            'approved_by' => $actorRef,
            'approved_at' => now(),
        ]);

        $request->update([
            'status' => InvestigationRequest::STATUS_COMPLETED,
            'completed_at' => now(),
            'approved_by' => $actorRef,
            'approved_at' => now(),
        ]);

        AuditLogger::record($ownerRef, 'investigation.approved', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);

        $request = $request->fresh(['patient', 'investigationType', 'result.values', 'visit']);
        $organization = Organization::query()->find($request->organization_id);
        if ($organization) {
            $this->queueBridge->complete($organization, $request);
            if ($request->visit) {
                $this->workflowCompletion->complete(
                    $organization,
                    $request->visit,
                    $this->queueBridge->contextForInvestigation($request),
                    $ownerRef,
                    $actorRef,
                );
            }
        }

        return $request;
    }

    public function deliver(InvestigationRequest $request, string $ownerRef, ?string $actorRef = null): InvestigationRequest
    {
        $this->assertStatus($request, InvestigationRequest::STATUS_COMPLETED);

        $request->update([
            'status' => InvestigationRequest::STATUS_DELIVERED,
            'delivered_at' => now(),
        ]);

        AuditLogger::record($ownerRef, 'investigation.delivered', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);

        return $request->fresh(['patient', 'investigationType', 'result']);
    }

    public function cancel(InvestigationRequest $request, string $ownerRef, ?string $actorRef = null): InvestigationRequest
    {
        if (in_array($request->status, [InvestigationRequest::STATUS_COMPLETED, InvestigationRequest::STATUS_DELIVERED], true)) {
            throw new InvalidArgumentException('Cannot cancel a completed investigation.');
        }

        $request->update(['status' => InvestigationRequest::STATUS_CANCELLED]);
        AuditLogger::record($ownerRef, 'investigation.cancelled', $request->organization_id, $actorRef, InvestigationRequest::class, $request->id);

        return $request->fresh();
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function createType(Organization $organization, string $ownerRef, array $data): InvestigationType
    {
        $type = InvestigationType::create([
            'owner_ref' => $ownerRef,
            'organization_id' => $organization->id,
            'name' => $data['name'],
            'code' => $data['code'] ?? null,
            'category' => $data['category'],
            'description' => $data['description'] ?? null,
            'unit' => $data['unit'] ?? null,
            'reference_low' => $data['reference_low'] ?? null,
            'reference_high' => $data['reference_high'] ?? null,
            'reference_text' => $data['reference_text'] ?? null,
            'price_minor' => (int) ($data['price_minor'] ?? 0),
            'is_active' => (bool) ($data['is_active'] ?? true),
        ]);

        AuditLogger::record($ownerRef, 'investigation_type.created', $organization->id, null, InvestigationType::class, $type->id);

        return $type;
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function updateType(InvestigationType $type, string $ownerRef, array $data): InvestigationType
    {
        $type->update([
            'name' => $data['name'],
            'code' => $data['code'] ?? null,
            'category' => $data['category'],
            'description' => $data['description'] ?? null,
            'unit' => $data['unit'] ?? null,
            'reference_low' => $data['reference_low'] ?? null,
            'reference_high' => $data['reference_high'] ?? null,
            'reference_text' => $data['reference_text'] ?? null,
            'price_minor' => (int) ($data['price_minor'] ?? 0),
            'is_active' => (bool) ($data['is_active'] ?? true),
        ]);

        AuditLogger::record($ownerRef, 'investigation_type.updated', $type->organization_id, null, InvestigationType::class, $type->id);

        return $type;
    }

    protected function assertStatus(InvestigationRequest $request, string ...$allowed): void
    {
        if (! in_array($request->status, $allowed, true)) {
            throw new InvalidArgumentException("Cannot transition from {$request->status}.");
        }
    }

    protected function isValueAbnormal(?string $value, mixed $low, mixed $high): bool
    {
        if ($value === null || $value === '') {
            return false;
        }

        if (! is_numeric($value)) {
            return false;
        }

        $numeric = (float) $value;

        if ($low !== null && $numeric < (float) $low) {
            return true;
        }

        if ($high !== null && $numeric > (float) $high) {
            return true;
        }

        return false;
    }

    protected function generateBarcode(InvestigationRequest $request): string
    {
        return 'LAB-'.str_pad((string) $request->id, 6, '0', STR_PAD_LEFT);
    }

    /**
     * @param  array<int, UploadedFile>  $files
     */
    protected function storeAttachments(InvestigationResult $result, string $ownerRef, array $files, ?string $actorRef): void
    {
        foreach ($files as $file) {
            if (! $file instanceof UploadedFile) {
                continue;
            }

            $path = $file->store("care/investigations/{$result->id}/attachments", 'public');

            InvestigationAttachment::create([
                'owner_ref' => $ownerRef,
                'investigation_result_id' => $result->id,
                'file_path' => $path,
                'original_name' => $file->getClientOriginalName(),
                'mime_type' => $file->getMimeType(),
                'uploaded_by' => $actorRef,
            ]);
        }
    }
}
