<?php

namespace App\Services\Care;

use App\Models\Assessment;
use App\Models\AssessmentAnswer;
use App\Models\AssessmentQuestion;
use App\Models\AssessmentTemplate;
use App\Models\Consultation;
use App\Models\Member;
use App\Models\Patient;
use App\Models\Visit;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;

class AssessmentService
{
    public function __construct(
        protected ScoringService $scoring,
    ) {}

    /**
     * Start a draft assessment for the current system template by code.
     * Idempotent: returns existing draft for the same patient/template/(consultation).
     *
     * @param  array{
     *     consultation?: ?Consultation,
     *     visit?: ?Visit,
     *     patient_pathway?: ?\App\Models\PatientPathway,
     *     practitioner_id?: ?int,
     *     actor?: ?string,
     * }  $context
     */
    public function start(
        Patient $patient,
        string $templateCode,
        string $ownerRef,
        ?Member $member,
        array $context = [],
    ): Assessment {
        $template = AssessmentTemplate::currentSystemByCode($templateCode);

        if (! $template) {
            throw ValidationException::withMessages([
                'template_code' => 'Unknown, inactive, or non-current assessment template.',
            ]);
        }

        $this->assertCaptureAllowed($member, $template);

        $consultation = $context['consultation'] ?? null;
        $visit = $context['visit'] ?? $consultation?->visit;
        $patientPathway = $context['patient_pathway'] ?? null;
        $actor = $context['actor'] ?? null;

        return DB::transaction(function () use ($patient, $template, $ownerRef, $consultation, $visit, $patientPathway, $context, $actor) {
            $query = Assessment::query()
                ->where('patient_id', $patient->id)
                ->where('template_id', $template->id)
                ->where('status', Assessment::STATUS_DRAFT)
                ->lockForUpdate();

            if ($consultation) {
                $query->where('consultation_id', $consultation->id);
            } else {
                $query->whereNull('consultation_id');
            }

            $existing = $query->first();
            if ($existing) {
                if ($patientPathway && ! $existing->patient_pathway_id) {
                    $existing->update(['patient_pathway_id' => $patientPathway->id]);
                }

                return $existing->load(['template.questions', 'answers.question', 'patient', 'consultation', 'visit', 'score']);
            }

            $branchId = $visit?->branch_id ?? $patient->branch_id;

            $assessment = Assessment::create([
                'owner_ref' => $ownerRef,
                'organization_id' => $patient->organization_id,
                'branch_id' => $branchId,
                'patient_id' => $patient->id,
                'template_id' => $template->id,
                'consultation_id' => $consultation?->id,
                'visit_id' => $visit?->id,
                'patient_pathway_id' => $patientPathway?->id,
                'practitioner_id' => $context['practitioner_id'] ?? $consultation?->practitioner_id,
                'status' => Assessment::STATUS_DRAFT,
                'started_by' => $actor,
            ]);

            AuditLogger::record(
                $ownerRef,
                'assessment.started',
                $patient->organization_id,
                $actor,
                Assessment::class,
                $assessment->id,
                ['template_code' => $template->code, 'template_version' => $template->version],
            );

            return $assessment->load(['template.questions', 'answers.question', 'patient', 'consultation', 'visit', 'score']);
        });
    }

    /**
     * @param  array<string, mixed>  $answers  keyed by question code
     */
    public function saveAnswers(
        Assessment $assessment,
        string $ownerRef,
        array $answers,
        ?Member $member = null,
        ?string $actorRef = null,
        ?string $notes = null,
    ): Assessment {
        $this->assertOwner($assessment, $ownerRef);
        $this->assertDraft($assessment);
        $assessment->loadMissing('template.questions');
        $this->assertCaptureAllowed($member, $assessment->template);

        DB::transaction(function () use ($assessment, $ownerRef, $answers, $notes) {
            foreach ($answers as $code => $raw) {
                $question = $assessment->template->questions->firstWhere('code', $code);
                if (! $question) {
                    throw ValidationException::withMessages([
                        "answers.{$code}" => "Unknown question code [{$code}].",
                    ]);
                }

                if ($question->answer_type === AssessmentQuestion::TYPE_CALCULATED) {
                    continue;
                }

                $normalized = $this->normalizeValue($question, $raw);

                AssessmentAnswer::updateOrCreate(
                    [
                        'assessment_id' => $assessment->id,
                        'question_id' => $question->id,
                    ],
                    array_merge(
                        ['owner_ref' => $ownerRef],
                        $normalized,
                    ),
                );
            }

            if ($notes !== null) {
                $assessment->update(['notes' => $notes]);
            }
        });

        AuditLogger::record(
            $ownerRef,
            'assessment.updated',
            $assessment->organization_id,
            $actorRef,
            Assessment::class,
            $assessment->id,
            ['template_code' => $assessment->template->code, 'answer_keys' => array_keys($answers)],
        );

        return $assessment->fresh(['template.questions', 'answers.question', 'patient', 'consultation', 'visit']);
    }

    public function complete(
        Assessment $assessment,
        string $ownerRef,
        ?Member $member = null,
        ?string $actorRef = null,
    ): Assessment {
        $this->assertOwner($assessment, $ownerRef);
        $this->assertDraft($assessment);
        $assessment->loadMissing(['template.questions', 'answers']);
        $this->assertCaptureAllowed($member, $assessment->template);

        $this->validateRequiredAnswers($assessment);

        return DB::transaction(function () use ($assessment, $ownerRef, $actorRef) {
            // Score first so complete fails cleanly without marking completed if scoring 422s.
            $scoreMeta = [];
            if ($this->scoring->requiresScore($assessment)) {
                $score = $this->scoring->materialize($assessment);
                $scoreMeta = [
                    'total_score' => $score->total_score,
                    'max_score' => $score->max_score,
                ];
            }

            $assessment->update([
                'status' => Assessment::STATUS_COMPLETED,
                'completed_at' => now(),
                'completed_by' => $actorRef,
                'assessed_at' => $assessment->assessed_at ?? now(),
            ]);

            AuditLogger::record(
                $ownerRef,
                'assessment.completed',
                $assessment->organization_id,
                $actorRef,
                Assessment::class,
                $assessment->id,
                array_merge(['template_code' => $assessment->template->code], $scoreMeta),
            );

            return $assessment->fresh(['template.questions', 'answers.question', 'patient', 'consultation', 'visit', 'score']);
        });
    }

    public function cancel(
        Assessment $assessment,
        string $ownerRef,
        ?Member $member = null,
        ?string $actorRef = null,
    ): Assessment {
        $this->assertOwner($assessment, $ownerRef);
        $this->assertDraft($assessment);
        $assessment->loadMissing('template');
        $this->assertCaptureAllowed($member, $assessment->template);

        $assessment->update([
            'status' => Assessment::STATUS_CANCELLED,
        ]);

        AuditLogger::record(
            $ownerRef,
            'assessment.cancelled',
            $assessment->organization_id,
            $actorRef,
            Assessment::class,
            $assessment->id,
            ['template_code' => $assessment->template->code],
        );

        return $assessment->fresh(['template', 'patient']);
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    public function listForPatient(
        Patient $patient,
        string $ownerRef,
        array $filters = [],
        ?int $branchScope = null,
    ): LengthAwarePaginator {
        $query = Assessment::query()
            ->owned($ownerRef)
            ->where('patient_id', $patient->id)
            ->with(['template', 'consultation', 'visit', 'practitioner'])
            ->orderByDesc('assessed_at')
            ->orderByDesc('created_at');

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

        if (! empty($filters['status'])) {
            $query->where('status', $filters['status']);
        }

        if (! empty($filters['template_code'])) {
            $query->whereHas('template', fn ($q) => $q->where('code', $filters['template_code']));
        }

        if (! empty($filters['category'])) {
            $query->whereHas('template', fn ($q) => $q->where('category', $filters['category']));
        }

        if (! empty($filters['from'])) {
            $query->whereDate('assessed_at', '>=', $filters['from']);
        }

        if (! empty($filters['to'])) {
            $query->whereDate('assessed_at', '<=', $filters['to']);
        }

        $perPage = min(max((int) ($filters['per_page'] ?? 20), 1), 100);

        return $query->paginate($perPage);
    }

    public function assertCaptureAllowed(?Member $member, AssessmentTemplate $template): void
    {
        if ($member === null) {
            throw new HttpException(403, 'Not authorized to capture assessments.');
        }

        if (in_array($member->role, ['hospital_admin', 'super_admin'], true)) {
            return;
        }

        $roles = $template->meta['capture_roles']
            ?? $this->defaultCaptureRoles($template->category);

        if (! in_array($member->role, $roles, true)) {
            throw new HttpException(403, 'Your role cannot capture this assessment template.');
        }
    }

    /**
     * @return list<string>
     */
    public function defaultCaptureRoles(string $category): array
    {
        return match ($category) {
            AssessmentTemplate::CATEGORY_DISEASE => [
                'doctor', 'general_physician', 'emergency_physician', 'surgeon',
                'pediatrician', 'oncologist', 'psychiatrist', 'dentist', 'pathologist',
            ],
            default => [
                'doctor', 'general_physician', 'emergency_physician', 'surgeon',
                'pediatrician', 'oncologist', 'psychiatrist', 'dentist', 'pathologist',
                'nurse', 'ed_nurse', 'theatre_nurse', 'dialysis_nurse', 'midwife',
                'physiotherapist', 'ambulance_staff', 'radiographer',
            ],
        };
    }

    /**
     * Normalize a raw answer into typed columns (exactly one authoritative column set).
     *
     * @return array{
     *     value_text: ?string,
     *     value_number: ?string|null,
     *     value_boolean: ?bool,
     *     value_date: ?string,
     *     value_json: mixed
     * }
     */
    public function normalizeValue(AssessmentQuestion $question, mixed $raw): array
    {
        $empty = [
            'value_text' => null,
            'value_number' => null,
            'value_boolean' => null,
            'value_date' => null,
            'value_json' => null,
        ];

        if ($raw === null || $raw === '') {
            return $empty;
        }

        $type = $question->answer_type;
        $options = $question->options ?? [];
        $validation = $question->validation ?? [];

        return match ($type) {
            AssessmentQuestion::TYPE_TEXT,
            AssessmentQuestion::TYPE_TEXTAREA => array_merge($empty, [
                'value_text' => $this->validatedString($raw, $question->code, $validation),
            ]),
            AssessmentQuestion::TYPE_SINGLE_CHOICE => array_merge($empty, [
                'value_text' => $this->validatedChoiceCode($raw, $question->code, $options),
            ]),
            AssessmentQuestion::TYPE_NUMBER,
            AssessmentQuestion::TYPE_INTEGER,
            AssessmentQuestion::TYPE_SCALE => array_merge($empty, [
                'value_number' => $this->validatedNumber($raw, $question->code, $options, $validation, $type === AssessmentQuestion::TYPE_INTEGER),
            ]),
            AssessmentQuestion::TYPE_SCORE_ITEM => array_merge($empty, [
                'value_number' => $this->validatedScoreItem($raw, $question->code, $options),
            ]),
            AssessmentQuestion::TYPE_BOOLEAN => array_merge($empty, [
                'value_boolean' => filter_var($raw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? (bool) $raw,
            ]),
            AssessmentQuestion::TYPE_DATE => array_merge($empty, [
                'value_date' => $this->validatedDate($raw, $question->code, false),
            ]),
            AssessmentQuestion::TYPE_DATETIME => array_merge($empty, [
                'value_date' => $this->validatedDate($raw, $question->code, true),
            ]),
            AssessmentQuestion::TYPE_MULTI_CHOICE => array_merge($empty, [
                'value_json' => $this->validatedMultiChoice($raw, $question->code, $options),
            ]),
            AssessmentQuestion::TYPE_CALCULATED => $empty,
            default => throw ValidationException::withMessages([
                "answers.{$question->code}" => "Unsupported answer type [{$type}].",
            ]),
        };
    }

    protected function assertOwner(Assessment $assessment, string $ownerRef): void
    {
        if ($assessment->owner_ref !== $ownerRef) {
            throw new HttpException(404, 'Assessment not found.');
        }
    }

    protected function assertDraft(Assessment $assessment): void
    {
        if ($assessment->status !== Assessment::STATUS_DRAFT) {
            throw ValidationException::withMessages([
                'status' => 'Only draft assessments can be modified. Completed assessments are immutable.',
            ]);
        }
    }

    protected function validateRequiredAnswers(Assessment $assessment): void
    {
        $answersByQuestion = $assessment->answers->keyBy('question_id');
        $errors = [];

        foreach ($assessment->template->questions as $question) {
            if ($question->answer_type === AssessmentQuestion::TYPE_CALCULATED) {
                continue;
            }

            if (! $question->is_required) {
                continue;
            }

            $answer = $answersByQuestion->get($question->id);
            if (! $answer || $this->isEmptyAuthoritative($answer, $question->answer_type)) {
                $errors["answers.{$question->code}"] = "{$question->label} is required.";
            }
        }

        if ($errors !== []) {
            throw ValidationException::withMessages($errors);
        }
    }

    protected function isEmptyAuthoritative(AssessmentAnswer $answer, string $type): bool
    {
        $value = $answer->authoritativeValue($type);

        if ($value === null || $value === '') {
            return true;
        }

        if (is_array($value) && $value === []) {
            return true;
        }

        return false;
    }

    protected function validatedString(mixed $raw, string $code, array $validation): string
    {
        $value = is_scalar($raw) ? (string) $raw : '';
        $max = (int) ($validation['max'] ?? 10000);
        if (mb_strlen($value) > $max) {
            throw ValidationException::withMessages([
                "answers.{$code}" => "Must be at most {$max} characters.",
            ]);
        }

        return $value;
    }

    protected function validatedChoiceCode(mixed $raw, string $code, array $options): string
    {
        $value = is_scalar($raw) ? (string) $raw : '';
        $choices = collect($options['choices'] ?? [])->pluck('code')->map(fn ($c) => (string) $c)->all();
        if ($choices !== [] && ! in_array($value, $choices, true)) {
            throw ValidationException::withMessages([
                "answers.{$code}" => 'Invalid choice.',
            ]);
        }

        return $value;
    }

    protected function validatedNumber(mixed $raw, string $code, array $options, array $validation, bool $integerOnly): string
    {
        if (! is_numeric($raw)) {
            throw ValidationException::withMessages([
                "answers.{$code}" => 'Must be a number.',
            ]);
        }

        if ($integerOnly && (float) $raw != (int) $raw) {
            throw ValidationException::withMessages([
                "answers.{$code}" => 'Must be an integer.',
            ]);
        }

        $number = (float) $raw;
        $min = $validation['min'] ?? $options['min'] ?? null;
        $max = $validation['max'] ?? $options['max'] ?? null;

        if ($min !== null && $number < (float) $min) {
            throw ValidationException::withMessages([
                "answers.{$code}" => "Must be at least {$min}.",
            ]);
        }
        if ($max !== null && $number > (float) $max) {
            throw ValidationException::withMessages([
                "answers.{$code}" => "Must be at most {$max}.",
            ]);
        }

        return (string) $number;
    }

    protected function validatedScoreItem(mixed $raw, string $code, array $options): string
    {
        $choices = $options['choices'] ?? [];
        if (is_array($choices) && $choices !== []) {
            // Accept choice code or numeric score.
            if (is_string($raw) || is_int($raw)) {
                $asString = (string) $raw;
                foreach ($choices as $choice) {
                    if ((string) ($choice['code'] ?? '') === $asString) {
                        if (! array_key_exists('score', $choice)) {
                            throw ValidationException::withMessages([
                                "answers.{$code}" => 'Choice has no score.',
                            ]);
                        }

                        return (string) $choice['score'];
                    }
                }
            }
        }

        return $this->validatedNumber($raw, $code, $options, [], false);
    }

    protected function validatedDate(mixed $raw, string $code, bool $withTime): string
    {
        $format = $withTime ? 'Y-m-d H:i:s' : 'Y-m-d';
        $rules = [$withTime ? 'date' : 'date_format:Y-m-d'];
        $validator = Validator::make(
            ['v' => $raw],
            ['v' => $rules],
        );
        if ($validator->fails()) {
            throw ValidationException::withMessages([
                "answers.{$code}" => 'Invalid date.',
            ]);
        }

        try {
            $dt = \Carbon\Carbon::parse($raw);

            return $withTime ? $dt->format('Y-m-d H:i:s') : $dt->format('Y-m-d').' 00:00:00';
        } catch (\Throwable) {
            throw ValidationException::withMessages([
                "answers.{$code}" => 'Invalid date.',
            ]);
        }
    }

    /**
     * @return list<string>
     */
    protected function validatedMultiChoice(mixed $raw, string $code, array $options): array
    {
        if (! is_array($raw)) {
            throw ValidationException::withMessages([
                "answers.{$code}" => 'Must be a list of choices.',
            ]);
        }

        $allowed = collect($options['choices'] ?? [])->pluck('code')->map(fn ($c) => (string) $c)->all();
        $values = array_values(array_map(fn ($v) => (string) $v, $raw));

        foreach ($values as $value) {
            if ($allowed !== [] && ! in_array($value, $allowed, true)) {
                throw ValidationException::withMessages([
                    "answers.{$code}" => "Invalid choice [{$value}].",
                ]);
            }
        }

        return $values;
    }
}
