<?php

namespace App\Services\Witness;

use App\Models\Claim\Claim;
use App\Models\Document\DocumentType;
use App\Models\Dsar\DsarEvidenceItem;
use App\Models\Witness\WitnessStatement;
use App\Models\Witness\WitnessStatementAnswer;
use App\Models\Witness\WitnessStatementExhibit;
use App\Models\Witness\WitnessStatementVersion;
use App\Services\HistoryService;
use App\Support\Witness\WitnessProgress;
use App\Support\Witness\WitnessQuestion;
use App\Support\Witness\WitnessQuestionnaire;
use App\Support\Witness\WitnessSection;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * All witness-statement business logic. Kept out of the components/Blade so the
 * four UI concepts share one authoritative implementation.
 *
 * It never runs evidence extraction — it consumes the existing confirmed
 * DsarEvidenceItem findings and maps them into suggested answers.
 */
class WitnessStatementService
{
    public function __construct(
        private readonly HistoryService $history,
        private readonly WitnessQuestionnaire $questionnaire,
        private readonly WitnessStatementRenderer $renderer,
    ) {}

    /** @var array<int, Collection<int, DsarEvidenceItem>> */
    private array $evidenceCache = [];

    /**
     * Get (or create) the single witness-statement workspace for a claim,
     * priming it with autofilled answers and evidence suggestions on first use.
     */
    public function resolve(Claim $claim): WitnessStatement
    {
        $ws = WitnessStatement::firstOrCreate(
            ['claim_id' => $claim->id],
            [
                'solicitor_id' => $claim->solicitor_id,
                'client_id' => $claim->client_id,
                'status' => 'draft',
                'generation_options' => $this->defaultOptions(),
            ]
        );

        if ($ws->wasRecentlyCreated) {
            $this->autopopulate($ws);
            $this->seedSuggestions($ws);
        }

        return $ws->fresh();
    }

    /**
     * Fill answers from existing client/claim data per the config autofill map.
     * Only fills questions that have no answer yet — never clobbers user input.
     */
    public function autopopulate(WitnessStatement $ws): void
    {
        $ws->loadMissing(['client', 'claim.careSettings.institution', 'claim.careSettings.area.council']);

        foreach ($this->questionnaire->autofillMappings() as [$sectionKey, $questionKey, $path]) {
            $value = $this->resolveAutofill($ws, $path);

            if ($value === null || $value === '' || $value === []) {
                continue;
            }

            $exists = $ws->answers()
                ->where('section_key', $sectionKey)
                ->where('question_key', $questionKey)
                ->exists();

            if ($exists) {
                continue;
            }

            $origin = str_starts_with($path, 'claim.') ? 'claim_data' : 'client_data';

            $ws->answers()->create([
                'solicitor_id' => $ws->solicitor_id,
                'section_key' => $sectionKey,
                'question_key' => $questionKey,
                'value' => $value,
                'origin' => $origin,
                'review_status' => 'manual',
            ]);
        }
    }

    /**
     * Materialise evidence-backed suggestions as `suggested` answer rows.
     * Idempotent: it will not overwrite a manual/accepted answer, and refreshes
     * an existing suggestion in place.
     */
    public function seedSuggestions(WitnessStatement $ws): void
    {
        $evidence = $this->confirmedEvidence($ws);

        foreach ($this->questionnaire->sections() as $section) {
            if (! $section->acceptsEvidence()) {
                continue;
            }

            $items = $evidence->filter(
                fn (DsarEvidenceItem $i) => in_array($i->category, $section->evidenceCategories, true)
            )->values();

            if ($items->isEmpty()) {
                continue;
            }

            $question = $this->narrativeQuestion($section);
            if (! $question) {
                continue;
            }

            $existing = $ws->answers()
                ->where('section_key', $section->key)
                ->where('question_key', $question->key)
                ->first();

            // Never overwrite a real user answer.
            if ($existing && ! in_array($existing->review_status, ['suggested'], true)) {
                continue;
            }

            $payload = [
                'value' => $this->draftFromItems($items),
                'origin' => 'evidence',
                'review_status' => 'suggested',
                'provenance' => $this->provenanceFromItems($items),
            ];

            if ($existing) {
                $existing->update($payload);
            } else {
                $ws->answers()->create(array_merge($payload, [
                    'solicitor_id' => $ws->solicitor_id,
                    'section_key' => $section->key,
                    'question_key' => $question->key,
                ]));
            }
        }
    }

    /**
     * Confirmed evidence findings for the claim — the "relevant" evidence that
     * came out of Evidence Review. Read-only; scoped to the claim's DSARs.
     *
     * @return Collection<int, DsarEvidenceItem>
     */
    public function confirmedEvidence(WitnessStatement $ws): Collection
    {
        // Memoised per request: progress(), suggestions and the evidence rail all
        // ask for this, so compute the query once.
        if (isset($this->evidenceCache[$ws->id])) {
            return $this->evidenceCache[$ws->id];
        }

        $dsarIds = $ws->claim->dsars()->pluck('id');

        if ($dsarIds->isEmpty()) {
            return $this->evidenceCache[$ws->id] = collect();
        }

        return $this->evidenceCache[$ws->id] = DsarEvidenceItem::query()
            ->whereIn('dsar_id', $dsarIds)
            ->confirmed()
            ->with(['document', 'careSetting.institution'])
            ->orderByDesc('severity')
            ->get();
    }

    /**
     * Confirmed evidence relevant to a given section (matched on the section's
     * evidence categories). Drives the contextual evidence rail.
     *
     * @return Collection<int, DsarEvidenceItem>
     */
    public function sectionEvidence(WitnessStatement $ws, ?string $sectionKey): Collection
    {
        $section = $sectionKey ? $this->questionnaire->section($sectionKey) : null;

        if (! $section || ! $section->acceptsEvidence()) {
            return collect();
        }

        return $this->confirmedEvidence($ws)
            ->filter(fn (DsarEvidenceItem $i) => in_array($i->category, $section->evidenceCategories, true))
            ->values();
    }

    /**
     * Save (create or update) a manual answer, recording provenance/origin and
     * auditing the change.
     */
    public function saveAnswer(WitnessStatement $ws, string $sectionKey, string $questionKey, mixed $value, string $origin = 'user'): WitnessStatementAnswer
    {
        return DB::transaction(function () use ($ws, $sectionKey, $questionKey, $value, $origin) {
            $answer = $ws->answers()->updateOrCreate(
                ['section_key' => $sectionKey, 'question_key' => $questionKey],
                [
                    'solicitor_id' => $ws->solicitor_id,
                    'value' => $value,
                    'origin' => $origin,
                    'review_status' => 'manual',
                    'answered_by' => auth()->id(),
                ]
            );

            $this->touchStatus($ws);
            $this->log($ws, 'Witness statement answer updated', [
                'section' => $sectionKey,
                'question' => $questionKey,
            ]);

            return $answer;
        });
    }

    public function acceptSuggestion(WitnessStatement $ws, WitnessStatementAnswer $answer): WitnessStatementAnswer
    {
        return DB::transaction(function () use ($ws, $answer) {
            $answer->update([
                'review_status' => 'accepted',
                'answered_by' => auth()->id(),
            ]);

            $this->touchStatus($ws);
            $this->log($ws, 'Suggested answer accepted', [
                'section' => $answer->section_key,
                'question' => $answer->question_key,
            ]);

            return $answer->refresh();
        });
    }

    public function rejectSuggestion(WitnessStatement $ws, WitnessStatementAnswer $answer): WitnessStatementAnswer
    {
        return DB::transaction(function () use ($ws, $answer) {
            $answer->update([
                'review_status' => 'rejected',
                'answered_by' => auth()->id(),
            ]);

            $this->touchStatus($ws);
            $this->log($ws, 'Suggested answer rejected', [
                'section' => $answer->section_key,
                'question' => $answer->question_key,
            ]);

            return $answer->refresh();
        });
    }

    /**
     * Accept a suggestion but with edited content (keeps the evidence
     * provenance so traceability survives the edit).
     */
    public function replaceSuggestion(WitnessStatement $ws, WitnessStatementAnswer $answer, mixed $value): WitnessStatementAnswer
    {
        return DB::transaction(function () use ($ws, $answer, $value) {
            $answer->update([
                'value' => $value,
                'review_status' => 'edited',
                'answered_by' => auth()->id(),
            ]);

            $this->touchStatus($ws);
            $this->log($ws, 'Suggested answer edited', [
                'section' => $answer->section_key,
                'question' => $answer->question_key,
            ]);

            return $answer->refresh();
        });
    }

    /**
     * Promote a confirmed evidence item (or a document page) to an exhibit.
     * Idempotent on (statement, document, page).
     */
    public function addExhibit(WitnessStatement $ws, array $data): WitnessStatementExhibit
    {
        return DB::transaction(function () use ($ws, $data) {
            // Key on the evidence item when present (each confirmed finding is
            // independently exhibitable, even two on the same page); otherwise
            // fall back to the document+page pair for manual exhibits.
            $match = ! empty($data['dsar_evidence_item_id'])
                ? ['dsar_evidence_item_id' => $data['dsar_evidence_item_id']]
                : ['document_id' => $data['document_id'] ?? null, 'page' => $data['page'] ?? null];

            $exhibit = $ws->exhibits()->firstOrCreate(
                $match,
                [
                    'solicitor_id' => $ws->solicitor_id,
                    'dsar_evidence_item_id' => $data['dsar_evidence_item_id'] ?? null,
                    'document_id' => $data['document_id'] ?? null,
                    'page' => $data['page'] ?? null,
                    'exhibit_ref' => $data['exhibit_ref'] ?? $this->nextExhibitRef($ws),
                    'label' => $data['label'] ?? null,
                    'added_by' => auth()->id(),
                ]
            );

            if ($exhibit->wasRecentlyCreated) {
                $this->log($ws, 'Exhibit added', [
                    'document_id' => $exhibit->document_id,
                    'page' => $exhibit->page,
                    'exhibit_ref' => $exhibit->exhibit_ref,
                ]);
            }

            return $exhibit;
        });
    }

    public function removeExhibit(WitnessStatement $ws, WitnessStatementExhibit $exhibit): void
    {
        DB::transaction(function () use ($ws, $exhibit) {
            $ref = $exhibit->exhibit_ref;
            $docId = $exhibit->document_id;
            $page = $exhibit->page;

            $exhibit->delete();

            $this->log($ws, 'Exhibit removed', [
                'document_id' => $docId,
                'page' => $page,
                'exhibit_ref' => $ref,
            ]);
        });
    }

    /**
     * Compute the progress / readiness snapshot.
     */
    public function progress(WitnessStatement $ws): WitnessProgress
    {
        $answers = $ws->relationLoaded('answers') ? $ws->answers : $ws->answers()->get();
        $byKey = $answers->keyBy(fn (WitnessStatementAnswer $a) => $a->section_key.'.'.$a->question_key);

        $total = 0;
        $answered = 0;
        $requiredTotal = 0;
        $requiredAnswered = 0;
        $requiredUnanswered = [];
        $sections = [];
        $groups = [];
        $awaitingReview = 0;
        $acceptedSuggestions = 0;
        $suggestionsTotal = 0;
        $conflicts = [];
        $missingEvidence = [];

        $evidenceCounts = $this->evidenceCategoryCounts($ws);

        foreach ($this->questionnaire->sections() as $section) {
            $sTotal = 0;
            $sAnswered = 0;
            $sRequiredUnanswered = 0;
            $sAwaitingReview = 0;

            foreach ($section->questions as $question) {
                $total++;
                $sTotal++;

                $answer = $byKey->get($section->key.'.'.$question->key);
                $isAnswered = $answer
                    && ! in_array($answer->review_status, ['suggested', 'rejected'], true)
                    && $question->isAnswered($answer->value);

                if ($answer?->origin === 'evidence') {
                    $suggestionsTotal++;
                }
                if ($answer?->review_status === 'suggested') {
                    $awaitingReview++;
                    $sAwaitingReview++;
                }
                if (in_array($answer?->review_status, ['accepted', 'edited'], true)) {
                    $acceptedSuggestions++;
                }

                if ($isAnswered) {
                    $answered++;
                    $sAnswered++;
                }

                if ($question->required) {
                    $requiredTotal++;
                    if ($isAnswered) {
                        $requiredAnswered++;
                    } else {
                        $sRequiredUnanswered++;
                        $requiredUnanswered[] = [
                            'section' => $section->key,
                            'question' => $question->key,
                            'label' => $question->label,
                        ];
                    }
                }
            }

            $sections[$section->key] = [
                'answered' => $sAnswered,
                'total' => $sTotal,
                'percent' => $sTotal ? (int) round(($sAnswered / $sTotal) * 100) : 0,
                'required_unanswered' => $sRequiredUnanswered,
                'awaiting_review' => $sAwaitingReview,
                'group' => $section->group,
            ];

            $g = $groups[$section->group] ?? ['answered' => 0, 'total' => 0, 'percent' => 0];
            $g['answered'] += $sAnswered;
            $g['total'] += $sTotal;
            $g['percent'] = $g['total'] ? (int) round(($g['answered'] / $g['total']) * 100) : 0;
            $groups[$section->group] = $g;

            // Missing supporting evidence: an evidence-backed section with no confirmed findings.
            if ($section->acceptsEvidence()) {
                $has = collect($section->evidenceCategories)->sum(fn ($c) => $evidenceCounts[$c] ?? 0);
                if ($has === 0) {
                    $missingEvidence[] = $section->title;
                }
            }
        }

        // Eligibility conflicts — genuine blockers for a Scottish Redress statement.
        $conflicts = $this->eligibilityConflicts($byKey);

        return new WitnessProgress(
            totalQuestions: $total,
            answeredQuestions: $answered,
            requiredTotal: $requiredTotal,
            requiredAnswered: $requiredAnswered,
            requiredUnanswered: $requiredUnanswered,
            sections: $sections,
            groups: $groups,
            awaitingReview: $awaitingReview,
            acceptedSuggestions: $acceptedSuggestions,
            conflicts: $conflicts,
            missingEvidenceSections: $missingEvidence,
            exhibitCount: $ws->exhibits()->count(),
            suggestionsTotal: $suggestionsTotal,
        );
    }

    /**
     * Generate a new immutable version of the statement.
     *
     * Transactional and idempotent-by-version: each call produces the next
     * version and never mutates a prior one. The prior versions' snapshots are
     * plain JSON copies, so they never change when data changes later.
     */
    public function generate(WitnessStatement $ws, array $options = [], ?int $userId = null): WitnessStatementVersion
    {
        $ws->loadMissing(['answers', 'exhibits.document', 'client', 'claim']);
        $userId ??= auth()->id();
        $options = $this->normaliseOptions($options ?: ($ws->generation_options ?? []));

        return DB::transaction(function () use ($ws, $options, $userId) {
            $versionNumber = (int) ($ws->versions()->max('version_number') ?? 0) + 1;

            $answerSnapshot = $ws->answers
                ->reject(fn (WitnessStatementAnswer $a) => $a->review_status === 'rejected')
                ->mapWithKeys(fn (WitnessStatementAnswer $a) => [
                    $a->section_key.'.'.$a->question_key => [
                        'value' => $a->value,
                        'origin' => $a->origin,
                        'review_status' => $a->review_status,
                        'provenance' => $a->provenance,
                    ],
                ])->all();

            $exhibitSnapshot = $ws->exhibits->map(fn (WitnessStatementExhibit $e) => [
                'exhibit_ref' => $e->exhibit_ref,
                'document_id' => $e->document_id,
                'document_name' => $e->document?->display_name,
                'page' => $e->page,
                'label' => $e->label,
                'evidence_item_id' => $e->dsar_evidence_item_id,
            ])->all();

            $render = $this->renderer->render($ws, $versionNumber, $answerSnapshot, $exhibitSnapshot, $options);

            $document = $ws->claim->documents()->create([
                'solicitor_id' => $ws->solicitor_id,
                'user_id' => $userId,
                'type_id' => $this->documentType()->id,
                'display_name' => 'Witness Statement v'.$versionNumber,
                'file_name' => $render['file_name'],
                'file_type' => 'pdf',
                'file_size' => $render['file_size'],
            ]);

            $version = $ws->versions()->create([
                'solicitor_id' => $ws->solicitor_id,
                'version_number' => $versionNumber,
                'document_id' => $document->id,
                'answers_snapshot' => $answerSnapshot,
                'exhibits_snapshot' => $exhibitSnapshot,
                'options' => $options,
                'generated_by' => $userId,
                'generated_at' => now(),
            ]);

            $ws->update([
                'current_version_id' => $version->id,
                'status' => 'generated',
                'generation_options' => $options,
            ]);

            $event = $versionNumber === 1 ? 'Witness statement generated' : 'Witness statement regenerated';
            $this->log($ws, $event.' (v'.$versionNumber.')', [
                'version' => $versionNumber,
                'document_id' => $document->id,
            ], $userId);

            return $version;
        });
    }

    public function markForReview(WitnessStatement $ws): void
    {
        $ws->update(['status' => 'in_review']);
        $this->log($ws, 'Witness statement marked for review');
    }

    public function approve(WitnessStatement $ws): void
    {
        $ws->update(['status' => 'approved']);
        $this->log($ws, 'Witness statement approved');
    }

    // ── internals ────────────────────────────────────────────────────────────

    private function documentType(): DocumentType
    {
        return DocumentType::firstOrCreate(['display_name' => $this->questionnaire->documentTypeName()]);
    }

    private function narrativeQuestion(WitnessSection $section): ?WitnessQuestion
    {
        return $section->questions->firstWhere('type', 'long_text')
            ?? $section->questions->first();
    }

    /** @param Collection<int, DsarEvidenceItem> $items */
    private function draftFromItems(Collection $items): string
    {
        return $items
            ->map(fn (DsarEvidenceItem $i) => trim((string) $i->quote))
            ->filter()
            ->unique()
            ->implode("\n\n");
    }

    /** @param Collection<int, DsarEvidenceItem> $items */
    private function provenanceFromItems(Collection $items): array
    {
        $primary = $items->first();

        return [
            'dsar_evidence_item_id' => $primary?->id,
            'document_id' => $primary?->document_id,
            'document' => $primary?->document?->display_name,
            'page' => $primary?->page,
            'excerpt' => $primary?->quote,
            'severity' => $primary?->severity,
            'care_setting_id' => $primary?->care_setting_id,
            'supporting' => $items->map(fn (DsarEvidenceItem $i) => [
                'id' => $i->id,
                'document_id' => $i->document_id,
                'document' => $i->document?->display_name,
                'page' => $i->page,
                'excerpt' => $i->quote,
                'severity' => $i->severity,
                'category' => $i->category,
            ])->all(),
            'count' => $items->count(),
        ];
    }

    /** @return array<string,int> category => confirmed item count */
    private function evidenceCategoryCounts(WitnessStatement $ws): array
    {
        return $this->confirmedEvidence($ws)
            ->groupBy('category')
            ->map->count()
            ->all();
    }

    /**
     * @param  Collection<string, WitnessStatementAnswer>  $byKey
     * @return array<int, array{section:string,question:string,label:string}>
     */
    private function eligibilityConflicts(Collection $byKey): array
    {
        $conflicts = [];

        $checks = [
            ['eligibility.under_18', 'Applicant was not under 18 in this setting'],
            ['eligibility.in_scotland', 'Care setting was not in Scotland'],
        ];

        foreach ($checks as [$key, $label]) {
            $answer = $byKey->get($key);
            if ($answer && ! in_array($answer->review_status, ['suggested', 'rejected'], true) && $answer->value === false) {
                [$section, $question] = explode('.', $key);
                $conflicts[] = ['section' => $section, 'question' => $question, 'label' => $label];
            }
        }

        return $conflicts;
    }

    private function resolveAutofill(WitnessStatement $ws, string $path): mixed
    {
        $client = $ws->client;
        $claim = $ws->claim;

        return match ($path) {
            'client.full_name' => $client?->fullName ?: null,
            'client.date_of_birth' => $client?->date_of_birth ? (string) $client->date_of_birth->format('Y-m-d') : null,
            'client.current_address' => $this->addressString($client),
            'claim.case_number' => $claim?->case_number,
            'user.name' => auth()->user()?->name,
            'solicitor.name' => $ws->solicitor?->name,
            'claim.careSettings' => $this->careSettingRows($claim),
            default => null,
        };
    }

    private function addressString($client): ?string
    {
        if (! $client) {
            return null;
        }

        $address = $client->addresses()->where('is_current', true)->first()
            ?? $client->addresses()->latest()->first();

        if (! $address) {
            return null;
        }

        return collect([
            $address->line_1, $address->line_2, $address->line_3,
            $address->line_4, $address->postcode,
        ])->filter()->implode(', ') ?: null;
    }

    /** @return array<int, array<string, mixed>> */
    private function careSettingRows(?Claim $claim): array
    {
        if (! $claim) {
            return [];
        }

        return $claim->careSettings->map(fn ($cs) => [
            'setting' => $cs->institution?->display_name,
            'location' => $cs->areaName() ?? $cs->localAuthority(),
            'dates' => $cs->dates,
            'age' => null,
            'placing_authority' => $cs->localAuthority(),
            'notes' => null,
        ])->all();
    }

    private function defaultOptions(): array
    {
        $out = [];
        foreach ($this->questionnaire->generationOptions() as $opt) {
            $out[$opt['key']] = (bool) ($opt['default'] ?? false);
        }

        return $out;
    }

    /** @param array<string,mixed> $options */
    private function normaliseOptions(array $options): array
    {
        $out = [];
        foreach ($this->questionnaire->generationOptions() as $opt) {
            $out[$opt['key']] = (bool) ($options[$opt['key']] ?? ($opt['default'] ?? false));
        }

        return $out;
    }

    private function nextExhibitRef(WitnessStatement $ws): string
    {
        $count = $ws->exhibits()->count();

        return 'EX'.($count + 1);
    }

    private function touchStatus(WitnessStatement $ws): void
    {
        if ($ws->status === 'generated' || $ws->status === 'approved') {
            return;
        }

        if ($ws->status === 'draft') {
            $ws->update(['status' => 'in_review']);
        }
    }

    private function log(WitnessStatement $ws, string $event, array $context = [], ?int $userId = null): void
    {
        $context = array_merge([
            'witness_statement_id' => $ws->id,
            'claim_id' => $ws->claim_id,
        ], $context);

        $this->history->add($ws->client, $event, $context, $userId);
    }
}
