<?php

namespace Modules\ClinicCore\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\ClinicCore\Entities\ClcPatient;
use Modules\ClinicCore\Entities\ClcAppointment;
use Modules\ClinicCore\Entities\ClcSpecialty;
use Modules\ClinicCore\Entities\Services\ClinicCapabilities;

/**
 * Inline AI medical assistant for ClinicCore, powered by the shared AiCore
 * engine. Specialty-aware: the active specialty's terminology (from its driver)
 * is folded into the system prompt. Soft-guarded by available() so a missing or
 * disabled AiCore never fatals — endpoints return a friendly "not available".
 */
class ClcAiController extends Controller
{
    private const PROVIDER = '\Modules\AiCore\Entities\AiProvider';
    private const CLIENT   = '\Modules\AiCore\Entities\Services\AiClient';

    /**
     * Curated per-specialty clinical guidance folded into the system prompt
     * (red flags, common differentials, prescribing cautions). Keyed by
     * clc_specialties.code. A structural driver MAY override/extend this by
     * implementing `aiGuidance(): array` (soft-detected via method_exists),
     * exactly like the optional `capabilities()` hook. Config-only specialties
     * just fall back to this map; unknown codes contribute nothing.
     */
    private const DEFAULT_GUIDANCE = [
        'emergency_icu' => [
            'Always work A-B-C-D-E; surface any time-critical finding first (airway, shock, sepsis, stroke, ACS, major haemorrhage).',
            'State explicit red flags and the resuscitation/escalation step, not just the diagnosis.',
        ],
        'cardiology_vascular' => [
            'Treat chest pain as ACS until excluded; consider aortic dissection and PE in the differential.',
            'Flag QT-prolonging combinations and renal dosing for cardiac drugs.',
        ],
        'internal_medicine' => [
            'Reconcile polypharmacy and flag interactions; consider the commonest systemic causes before rare ones.',
            'Note any finding suggesting sepsis, malignancy, or acute organ failure as a red flag.',
        ],
        'pediatrics_newborn' => [
            'Weight-band all dosing (mg/kg) and flag any dose that needs a weight to be safe.',
            'Red flags: lethargy, poor feeding, non-blanching rash, dehydration, respiratory distress.',
        ],
        'dermatology' => [
            'Apply ABCDE to any pigmented lesion; flag rapid change, ulceration or bleeding for urgent referral.',
            'Distinguish drug eruptions and SJS/TEN red flags (mucosal involvement, skin pain, blistering).',
        ],
        'dental' => [
            'Flag spreading odontogenic infection, airway compromise (Ludwig\'s) and trismus as urgent.',
            'Note antibiotic stewardship — most localised pulpitis needs operative care, not antibiotics.',
        ],
        'gynaecology' => [
            'In any woman of reproductive age with abdominal pain or bleeding, exclude ectopic pregnancy first.',
            'Flag pre-eclampsia features (BP, headache, visual symptoms, epigastric pain); check every drug for pregnancy/breastfeeding safety.',
        ],
        'ivf' => [
            'Counsel and monitor for ovarian hyperstimulation syndrome (OHSS); flag severe abdominal pain/distension after stimulation.',
            'Confirm pregnancy location early to exclude ectopic/heterotopic pregnancy.',
        ],
        'psychiatry' => [
            'Always assess suicide/self-harm and risk to others; treat active risk as an emergency red flag.',
            'Watch for serotonin syndrome, NMS, and QT prolongation with psychotropics.',
        ],
        'ophthalmology' => [
            'Sudden painful red eye, sudden vision loss, or halos suggest acute glaucoma/retinal emergency — flag urgently.',
            'Distinguish sight-threatening causes (GCA, retinal detachment, endophthalmitis) early.',
        ],
        'neurology' => [
            'Sudden focal deficit = stroke pathway (time-critical); thunderclap headache = exclude SAH.',
            'Flag features of raised ICP and meningism as red flags.',
        ],
        'oncology' => [
            'Treat neutropenic sepsis, cord compression, SVC obstruction and hypercalcaemia as emergencies.',
            'Account for chemo interactions and renal/hepatic dose adjustment.',
        ],
        'orthopedics' => [
            'Flag open fracture, compartment syndrome, cauda equina and septic joint as surgical emergencies.',
        ],
        'gastroenterology' => [
            'Flag GI bleed, perforation and ascending cholangitis; note hepatic dosing for prescribed drugs.',
        ],
        'nephrology' => [
            'Adjust every drug for eGFR and flag nephrotoxic combinations; treat hyperkalaemia as urgent.',
        ],
        'chest_respiratory' => [
            'Flag hypoxia, silent chest, massive haemoptysis and tension pneumothorax as emergencies.',
            'Consider PE in any acute breathlessness with risk factors.',
        ],
        'endocrinology' => [
            'Treat DKA/HHS, myxoedema coma, thyroid storm and adrenal crisis as emergencies.',
            'Flag severe hypo/hyperglycaemia and sodium derangements.',
        ],
        'general_surgery' => [
            'Flag peritonitis, bowel obstruction/ischaemia and acute surgical abdomen for urgent assessment.',
        ],
        'urology' => [
            'Treat testicular torsion, obstructed infected kidney and urosepsis as emergencies; flag painless haematuria for cancer workup.',
        ],
        'hematology' => [
            'Flag febrile neutropenia, TTP, DIC and hyperviscosity as emergencies; review anticoagulation and bleeding risk.',
        ],
        'ent' => [
            'Flag airway compromise, epiglottitis, deep-neck space infection and uncontrolled epistaxis as urgent.',
        ],
        'rheumatology' => [
            'Exclude septic arthritis in any acute hot joint; flag giant-cell arteritis and steroid/immunosuppressant risks.',
        ],
    ];

    /** AiCore installed? */
    public static function available(): bool
    {
        return class_exists(self::PROVIDER) && class_exists(self::CLIENT);
    }

    private function guard(): void
    {
        if (!auth()->user()->can('clinic.use_ai_assist')) {
            abort(403, 'Unauthorized action.');
        }
    }

    public function ask(Request $request)
    {
        $this->guard();
        if (!self::available()) {
            return response()->json([
                'success' => false,
                'msg'     => 'AI assist is not available. Install & configure the AiCore module to enable it.',
            ], 422);
        }

        $request->validate([
            'mode'    => 'required|in:draft_soap,structure_soap,summarize_history,narrate_exam,differential,explain_to_patient,red_flags,patient_instructions,code_suggest,general',
            'details' => 'nullable|string|max:8000',
        ]);

        $businessId = (int) request()->session()->get('user.business_id');

        // Capability gate (specialty-aware when an appointment/patient is supplied).
        $specialtyId = $this->resolveSpecialtyId($request, $businessId);
        if (!ClinicCapabilities::enabled($businessId, $specialtyId, 'ai_assist')) {
            return response()->json(['success' => false, 'msg' => 'AI assist is disabled for this clinic type.'], 422);
        }

        $providerClass = self::PROVIDER;
        $clientClass   = self::CLIENT;

        $settings = $providerClass::forBusiness($businessId, 'default');
        if (method_exists($settings, 'isConfigured') && !$settings->isConfigured()) {
            return response()->json(['success' => false, 'msg' => 'AiCore is installed but no API key/model is configured. Open AiCore settings.'], 422);
        }
        if (method_exists($settings, 'budgetExceeded') && $settings->budgetExceeded()) {
            return response()->json(['success' => false, 'msg' => 'Monthly AI token budget exceeded.'], 422);
        }

        [$system, $userPrompt] = $this->buildPrompts($request, $businessId, $specialtyId, $settings);

        $res = app($clientClass)->chat($settings, $system, $userPrompt);
        if (empty($res['ok'])) {
            return response()->json(['success' => false, 'msg' => $res['error'] ?? 'AI request failed.'], 502);
        }

        return response()->json([
            'success'  => true,
            'response' => $res['text'],
            'tokens'   => ($res['tokens_in'] ?? 0) + ($res['tokens_out'] ?? 0),
        ]);
    }

    /**
     * Streaming variant of ask(): identical prompt-building, but the answer is
     * streamed token-by-token over text/event-stream so the assistant feels
     * instant. AiClient::stream falls back transparently to the resilient
     * non-stream path (with Groq->Bynara failover) if streaming is unavailable.
     */
    public function askStream(Request $request)
    {
        $this->guard();
        if (!self::available()) {
            return response()->json(['success' => false, 'msg' => 'AI assist is not available.'], 422);
        }
        $request->validate([
            'mode'    => 'required|in:draft_soap,structure_soap,summarize_history,narrate_exam,differential,explain_to_patient,red_flags,patient_instructions,code_suggest,general',
            'details' => 'nullable|string|max:8000',
        ]);
        $businessId  = (int) request()->session()->get('user.business_id');
        $specialtyId = $this->resolveSpecialtyId($request, $businessId);
        if (!ClinicCapabilities::enabled($businessId, $specialtyId, 'ai_assist')) {
            return response()->json(['success' => false, 'msg' => 'AI assist is disabled for this clinic type.'], 422);
        }
        $providerClass = self::PROVIDER;
        $clientClass   = self::CLIENT;
        $settings = $providerClass::forBusiness($businessId, 'default');
        if (method_exists($settings, 'isConfigured') && !$settings->isConfigured()) {
            return response()->json(['success' => false, 'msg' => 'AiCore is installed but no API key/model is configured.'], 422);
        }
        if (method_exists($settings, 'budgetExceeded') && $settings->budgetExceeded()) {
            return response()->json(['success' => false, 'msg' => 'Monthly AI token budget exceeded.'], 422);
        }

        [$system, $userPrompt] = $this->buildPrompts($request, $businessId, $specialtyId, $settings);
        $feature = 'assistant_' . $request->input('mode');

        return response()->stream(function () use ($clientClass, $settings, $system, $userPrompt, $feature) {
            @ini_set('output_buffering', 'off');
            @ini_set('zlib.output_compression', '0');
            while (ob_get_level() > 0) { @ob_end_flush(); }
            $send = function (string $event, array $data) {
                echo 'data: ' . json_encode(array_merge(['event' => $event], $data), JSON_UNESCAPED_UNICODE) . "\n\n";
                @ob_flush();
                @flush();
            };
            try {
                $res = app($clientClass)->stream($settings, $system, $userPrompt, function ($delta) use ($send) {
                    $send('delta', ['text' => (string) $delta]);
                }, ['module' => 'ClinicCore', 'feature' => $feature]);

                if (empty($res['ok'])) {
                    $send('error', ['msg' => $res['error'] ?? 'AI request failed.']);
                } else {
                    $send('done', ['tokens' => (int) (($res['tokens_in'] ?? 0) + ($res['tokens_out'] ?? 0))]);
                }
            } catch (\Throwable $e) {
                $send('error', ['msg' => 'AI request failed.']);
            }
        }, 200, [
            'Content-Type'      => 'text/event-stream',
            'Cache-Control'     => 'no-cache',
            'X-Accel-Buffering' => 'no',
        ]);
    }

    /**
     * Clean a recognised-text transcript from the browser Web Speech path
     * (which never sends audio to the server) so it gets the same non-medical
     * side-talk stripping as a server-transcribed dictation. Runs on the shared
     * AiCore engine. No-ops (returns the text unchanged) when AiCore is
     * unavailable/unconfigured or the clean toggle is off, so the front-end can
     * call it unconditionally.
     */
    public function cleanTranscript(Request $request)
    {
        if (!auth()->user()->can('clinic.use_ai_assist')) {
            abort(403, 'Unauthorized action.');
        }
        $request->validate(['text' => 'required|string|max:20000']);
        $raw = (string) $request->input('text');
        $passthrough = ['success' => true, 'text' => $raw, 'raw_text' => $raw, 'cleaned' => false];

        if (!self::available() || trim($raw) === '') {
            return response()->json($passthrough);
        }

        $businessId    = (int) request()->session()->get('user.business_id');
        $providerClass = self::PROVIDER;
        $clientClass   = self::CLIENT;
        $settings      = $providerClass::forBusiness($businessId, 'default');

        if (empty($settings->t_clean_non_medical)
            || (method_exists($settings, 'isConfigured') && !$settings->isConfigured())
            || mb_strlen(trim($raw)) < 25) {
            return response()->json($passthrough);
        }

        $system = $settings->prompt('clinic.clean');
        $user = "Transcript:\n---\n{$raw}\n---";

        $res = app($clientClass)->chat($settings, $system, $user, ['module' => 'ClinicCore', 'feature' => 'clean_transcript']);
        if (empty($res['ok'])) {
            return response()->json($passthrough + ['clean_note' => $res['error'] ?? 'llm-failed']);
        }
        $cleaned = trim((string) ($res['text'] ?? ''));

        // Defensive: if the model stripped almost everything (>80%), keep the
        // original so a clinician never silently loses dictated content.
        if ($cleaned !== '' && mb_strlen($cleaned) < (int) (mb_strlen($raw) * 0.20) && mb_strlen($raw) > 200) {
            return response()->json($passthrough + ['clean_note' => 'cleaned-too-short-fallback']);
        }

        return response()->json([
            'success'  => true,
            'text'     => $cleaned,
            'raw_text' => $raw,
            'cleaned'  => $cleaned !== $raw,
        ]);
    }

    private function resolveSpecialtyId(Request $request, int $businessId): ?int
    {
        if ($request->filled('appointment_id')) {
            $apt = ClcAppointment::where('business_id', $businessId)->find($request->appointment_id);
            if ($apt && $apt->specialty_id) return (int) $apt->specialty_id;
        }
        if ($request->filled('patient_id')) {
            $p = ClcPatient::where('business_id', $businessId)->find($request->patient_id);
            if ($p && $p->default_specialty_id) return (int) $p->default_specialty_id;
        }
        return null;
    }

    /**
     * Per-specialty clinical guidance: an optional driver `aiGuidance()` wins,
     * otherwise the curated DEFAULT_GUIDANCE map keyed by the specialty code.
     *
     * @return array<int,string>
     */
    private function resolveGuidance(ClcSpecialty $specialty): array
    {
        try {
            if ($specialty->hasDriver() && ($driver = $specialty->driver())
                && method_exists($driver, 'aiGuidance')) {
                $g = $driver->aiGuidance();
                if (is_array($g) && $g) return array_values(array_filter(array_map('strval', $g)));
            }
        } catch (\Throwable $e) { /* fall back to the static map */ }

        return self::DEFAULT_GUIDANCE[$specialty->code] ?? [];
    }

    private function buildPrompts(Request $request, int $businessId, ?int $specialtyId, $settings): array
    {
        $specialtyName = 'general medicine';
        $glossary = '';
        $guidance = [];
        if ($specialtyId) {
            $specialty = ClcSpecialty::find($specialtyId);
            if ($specialty) {
                $specialtyName = $specialty->name;
                $guidance = $this->resolveGuidance($specialty);
                try {
                    if ($specialty->hasDriver() && ($driver = $specialty->driver())) {
                        $terms = $driver->terminology();
                        if (is_array($terms) && $terms) {
                            $glossary = ' Specialty terminology: ' . collect($terms)
                                ->map(fn($v, $k) => "$k=$v")->implode(', ') . '.';
                        }
                    }
                } catch (\Throwable $e) { /* ignore driver issues */ }
            }
        }

        $system = $settings->prompt('clinic.assist', ['specialty' => $specialtyName]) . $glossary;

        if ($guidance) {
            $system .= ' Specialty clinical notes (apply when relevant): - ' . implode(' - ', $guidance);
        }

        $context = $this->patientContext($request, $businessId);
        $details = trim((string) $request->input('details', ''));
        $mode = $request->input('mode');

        switch ($mode) {
            case 'draft_soap':
                $user = "Draft a structured SOAP note from the information below. Output ONLY the four "
                      . "sections, each on its own block prefixed EXACTLY with these markers so the app can "
                      . "parse them: [SUBJECTIVE], [OBJECTIVE], [ASSESSMENT], [PLAN].\n\n{$context}\nClinician input: {$details}";
                break;
            case 'structure_soap':
                $user = "Reorganise the following consultation dictation/notes into a SOAP note. Do not add "
                      . "facts that are not present. Output ONLY the four sections, each prefixed EXACTLY with "
                      . "[SUBJECTIVE], [OBJECTIVE], [ASSESSMENT], [PLAN].\n\n{$context}\nDictation: {$details}";
                break;
            case 'summarize_history':
                $user = "Summarise this patient's relevant medical history into a concise clinical brief, "
                      . "highlighting allergies, chronic conditions and any red flags.\n\n{$context}\n{$details}";
                break;
            case 'narrate_exam':
                $user = "Turn these examination findings into a clear clinical narrative for the record.\n\n{$context}\nFindings: {$details}";
                break;
            case 'differential':
                $user = "Given the presentation below, propose a prioritised differential diagnosis with brief "
                      . "reasoning and suggested next investigations. Flag any emergencies.\n\n{$context}\nPresentation: {$details}";
                break;
            case 'explain_to_patient':
                $user = "Rewrite the following in plain, reassuring language a patient can understand, "
                      . "avoiding jargon.\n\n{$context}\nContent: {$details}";
                break;
            case 'red_flags':
                $user = "Review the information below and list any RED-FLAG or urgent/emergency findings "
                      . "that need immediate action, most critical first, each with the recommended next "
                      . "step. If nothing urgent is evident, say so explicitly. Do not invent findings.\n\n"
                      . "{$context}\nPresentation / notes: {$details}";
                break;
            case 'patient_instructions':
                $user = "Write clear after-visit instructions for the patient in plain, reassuring language. "
                      . "Use short bullet points covering: what was found, what to do at home, how to take any "
                      . "medication, warning signs that mean they should seek urgent care, and follow-up. "
                      . "Base it only on the information supplied.\n\n{$context}\nPlan / details: {$details}";
                break;
            case 'code_suggest':
                $user = "Suggest the most likely ICD-10 diagnosis codes and, where relevant, CPT/procedure "
                      . "codes for this encounter, each with a one-line justification. These are suggestions "
                      . "for the clinician to confirm — do not fabricate codes for unsupported diagnoses.\n\n"
                      . "{$context}\nAssessment / details: {$details}";
                break;
            default:
                $user = "{$context}\n{$details}";
        }

        return [$system, $user];
    }

    private function patientContext(Request $request, int $businessId): string
    {
        $parts = [];
        if ($request->filled('patient_id')) {
            $patient = ClcPatient::where('business_id', $businessId)->find($request->patient_id);
            if ($patient) {
                $parts[] = 'Patient: ' . ($patient->patient_name ?: ('#' . $patient->id));
                foreach (['allergies' => 'Allergies', 'chronic_conditions' => 'Chronic conditions',
                          'medical_alerts' => 'Medical alerts', 'family_history' => 'Family history'] as $col => $label) {
                    if (!empty($patient->{$col})) $parts[] = "$label: {$patient->{$col}}";
                }

                if ($meds = $this->currentMedications($patient->id, $businessId)) {
                    $parts[] = "Current medications: {$meds}";
                }
                if ($labs = $this->recentAbnormalLabs($patient->id)) {
                    $parts[] = "Recent abnormal labs: {$labs}";
                }
            }
        }
        if ($request->filled('appointment_id')) {
            $apt = ClcAppointment::where('business_id', $businessId)->find($request->appointment_id);
            if ($apt) {
                $vitals = collect([
                    'Temp' => $apt->temperature, 'HR' => $apt->heart_rate, 'RR' => $apt->respiratory_rate,
                    'BP' => ($apt->systolic_bp ?? null) ? "{$apt->systolic_bp}/{$apt->diastolic_bp}" : null,
                    'SpO2' => $apt->spo2 ?? null, 'Glucose' => $apt->blood_glucose ?? null, 'BMI' => $apt->bmi ?? null,
                    'Weight' => $apt->weight_kg,
                ])->filter()->map(fn($v, $k) => "$k $v")->implode(', ');
                if ($vitals) $parts[] = "Vitals: {$vitals}";
                if (!empty($apt->soap_subjective)) $parts[] = "Subjective: {$apt->soap_subjective}";

                if (\Schema::hasTable('clc_appointment_exam_findings')) {
                    $findings = \DB::table('clc_appointment_exam_findings')
                        ->where('appointment_id', $apt->id)
                        ->limit(40)->pluck('item_label_snapshot')->filter()->implode('; ');
                    if ($findings) $parts[] = "Exam findings: {$findings}";
                }
            }
        }
        return $parts ? (implode('. ', $parts) . '.') : '';
    }

    /** Active-prescription medications for the patient (guarded; empty if tables absent). */
    private function currentMedications(int $patientId, int $businessId): string
    {
        if (!\Schema::hasTable('clc_prescriptions') || !\Schema::hasTable('clc_prescription_items')) {
            return '';
        }
        try {
            $rows = \DB::table('clc_prescription_items as i')
                ->join('clc_prescriptions as p', 'p.id', '=', 'i.prescription_id')
                ->where('p.business_id', $businessId)
                ->where('p.patient_id', $patientId)
                ->where('p.status', 'active')
                ->orderByDesc('p.prescribed_date')
                ->limit(20)
                ->get(['i.medicine_name', 'i.dosage', 'i.frequency']);
        } catch (\Throwable $e) {
            return '';
        }
        return $rows->map(function ($r) {
            $s = $r->medicine_name;
            if (!empty($r->dosage))    $s .= " {$r->dosage}";
            if (!empty($r->frequency)) $s .= " {$r->frequency}";
            return trim($s);
        })->filter()->unique()->implode('; ');
    }

    /** Most recent abnormal lab results for the patient via ClinicLab linkage (guarded). */
    private function recentAbnormalLabs(int $patientId): string
    {
        if (!\Schema::hasTable('lab_test_sessions') || !\Schema::hasTable('lab_test_results')
            || !\Schema::hasColumn('lab_test_sessions', 'clc_patient_id')) {
            return '';
        }
        try {
            $rows = \DB::table('lab_test_results as r')
                ->join('lab_test_sessions as s', 's.id', '=', 'r.test_session_id')
                ->where('s.clc_patient_id', $patientId)
                ->where('r.is_abnormal', 1)
                ->orderByDesc('s.id')
                ->limit(15)
                ->get(['r.analyte_name', 'r.value', 'r.unit', 'r.flags']);
        } catch (\Throwable $e) {
            return '';
        }
        return $rows->map(function ($r) {
            $s = $r->analyte_name . ' ' . $r->value . (!empty($r->unit) ? " {$r->unit}" : '');
            if (!empty($r->flags)) $s .= " ({$r->flags})";
            return trim($s);
        })->filter()->implode('; ');
    }
}
