<?php
/**
 * NIS2 Agile - Gap Analysis ACN (Determinazione 164179/2025)
 * ----------------------------------------------------------------------------
 * Assessment di conformita di SECONDO LIVELLO: misure e requisiti puntuali
 * della Determinazione ACN n. 164179/2025 (Allegati 1 e 2), NON le 10 lettere
 * generiche dell'Art. 21 NIS2 (quelle restano in AssessmentController).
 *
 *   - soggetti IMPORTANTI (Allegato 1): 37 misure, 87 requisiti
 *   - soggetti ESSENZIALI (Allegato 2): 43 misure, 116 requisiti (= 87 + 29)
 *
 * La codifica dei requisiti e identica per i due livelli; agli essenziali si
 * aggiungono misure e punti. Il perimetro applicabile e filtrato in base al
 * livello del soggetto (acn_assessments.entity_level, derivato da
 * organizations.entity_type).
 *
 * Fonte testi: application/data/acn_measures.json (estratto integrale ACN).
 *
 * Endpoint (base /api/acn-gap):
 *   GET  /catalog              - catalogo misure/requisiti per il livello dell'org
 *   GET  /list                 - assessment ACN dell'org
 *   POST /create               - crea assessment (pre-popola requisiti applicabili)
 *   GET  /{id}                 - intestazione + stats
 *   GET  /{id}/requirements    - requisiti raggruppati per funzione/categoria/misura
 *   POST /{id}/respond         - salva risposta (singola o batch)
 *   POST /{id}/complete        - calcola score e completa
 *   GET  /{id}/report          - report completo
 *   POST /{id}/aiAnalyze       - analisi AI con grounding sui requisiti ACN
 */

require_once __DIR__ . '/BaseController.php';

class AcnAssessmentController extends BaseController
{
    /** Cache in-process del dataset misure. */
    private ?array $measuresCache = null;

    // ════════════════════════ CATALOGO ════════════════════════

    /**
     * GET /api/acn-gap/catalog
     * Catalogo dei requisiti ACN applicabili al livello del soggetto corrente
     * (importante/essenziale), raggruppato per funzione FW. Sola lettura,
     * non crea assessment. Utile per anteprima/consultazione normativa.
     */
    public function catalog(): void
    {
        $this->requireOrgAccess();
        $level = $this->resolveEntityLevel();
        if ($level === null) {
            // Coerente con create(): senza classificazione non c'e perimetro.
            // Il frontend intercetta ENTITY_LEVEL_REQUIRED e mostra l'invito a classificare.
            $this->jsonError(
                'Il soggetto non e classificato come importante o essenziale. Completa prima la classificazione NIS2 dell\'organizzazione.',
                422,
                'ENTITY_LEVEL_REQUIRED'
            );
        }
        $grouped = $this->groupedRequirements($level, []);

        $totals = $this->datasetTotals();
        $this->jsonSuccess([
            'entity_level' => $level,
            'totals'       => $totals[$level],
            'functions'    => $grouped,
            'source'       => $this->measures()['source'] ?? null,
        ]);
    }

    // ════════════════════════ LISTA / CREATE ════════════════════════

    /** GET /api/acn-gap/list */
    public function list(): void
    {
        $this->requireOrgAccess();
        $orgId = $this->getCurrentOrgId();
        $rows = Database::fetchAll(
            'SELECT id, title, entity_level, status, overall_score, completed_at, created_at
               FROM acn_assessments
              WHERE organization_id = ?
              ORDER BY created_at DESC',
            [$orgId]
        );
        $this->jsonSuccess(['assessments' => $rows]);
    }

    /**
     * POST /api/acn-gap/create  Body: { title? }
     * Crea un assessment ACN e pre-popola TUTTI i requisiti applicabili al
     * livello del soggetto (snapshot del testo congelato).
     */
    public function create(): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $orgId  = $this->getCurrentOrgId();
        $userId = $this->getCurrentUserId();
        $level  = $this->resolveEntityLevel();

        if ($level === null) {
            $this->jsonError(
                'Il soggetto non e classificato come importante o essenziale. Completa prima la classificazione NIS2 dell\'organizzazione.',
                422,
                'ENTITY_LEVEL_REQUIRED'
            );
        }

        $title = trim((string) $this->getParam('title', ''));
        if ($title === '') {
            $title = 'Gap Analysis ACN ' . date('Y-m-d');
        }

        Database::beginTransaction();
        try {
            $assessmentId = Database::insert('acn_assessments', [
                'organization_id' => $orgId,
                'title'           => $title,
                'entity_level'    => $level,
                'status'          => 'draft',
                'created_by'      => $userId,
            ]);

            $applicable = $this->applicableRequirements($level);
            foreach ($applicable as $r) {
                Database::insert('acn_assessment_responses', [
                    'assessment_id'     => $assessmentId,
                    'organization_id'   => $orgId,
                    'requirement_key'   => $r['key'],
                    'measure_code'      => $r['measure_code'],
                    'requirement_index' => $r['index'],
                    'function_code'     => $r['function_code'],
                    'category_code'     => $r['category_code'],
                    'requirement_text'  => $r['text'],
                    'response_value'    => null,
                ]);
            }
            Database::commit();
        } catch (Throwable $e) {
            if (Database::getInstance()->inTransaction()) {
                Database::rollback();
            }
            throw $e;
        }

        $this->logAudit('acn_assessment_created', 'acn_assessment', $assessmentId, [
            'entity_level' => $level,
            'requirements' => count($applicable),
        ]);

        $this->jsonSuccess([
            'id'           => $assessmentId,
            'entity_level' => $level,
            'requirements' => count($applicable),
        ], 'Assessment ACN creato', 201);
    }

    // ════════════════════════ DETTAGLIO ════════════════════════

    /** GET /api/acn-gap/{id} */
    public function get(int $id): void
    {
        $this->requireOrgAccess();
        $a = $this->loadOwned($id);
        $a['stats'] = $this->computeStats($id);
        $this->jsonSuccess($a);
    }

    /**
     * GET /api/acn-gap/{id}/requirements
     * Requisiti raggruppati per funzione -> categoria -> misura, con la risposta
     * salvata. Struttura pensata per il rendering a wizard/accordion.
     */
    public function getRequirements(int $id): void
    {
        $this->requireOrgAccess();
        $a = $this->loadOwned($id);

        $rows = Database::fetchAll(
            'SELECT requirement_key, measure_code, requirement_index, function_code,
                    category_code, requirement_text, response_value, evidence_description, notes, answered_at
               FROM acn_assessment_responses
              WHERE assessment_id = ?
              ORDER BY id',
            [$id]
        );

        // arricchimento da dataset: titoli misura/categoria/funzione
        $meta = $this->measureMeta();
        $byFunc = [];
        foreach ($rows as $r) {
            $fc = $r['function_code'];
            $cc = $r['category_code'];
            $mc = $r['measure_code'];
            $byFunc[$fc] ??= [
                'function_code'  => $fc,
                'function_name'  => $meta['functions'][$fc] ?? $fc,
                'categories'     => [],
            ];
            $byFunc[$fc]['categories'][$cc] ??= [
                'category_code'  => $cc,
                'category_title' => $meta['categories'][$cc] ?? $cc,
                'measures'       => [],
            ];
            $byFunc[$fc]['categories'][$cc]['measures'][$mc] ??= [
                'measure_code'  => $mc,
                'measure_title' => $meta['measures'][$mc] ?? $mc,
                'requirements'  => [],
            ];
            $byFunc[$fc]['categories'][$cc]['measures'][$mc]['requirements'][] = [
                'key'          => $r['requirement_key'],
                'index'        => (int) $r['requirement_index'],
                'text'         => $r['requirement_text'],
                'response'     => $r['response_value'],
                'evidence'     => $r['evidence_description'],
                'notes'        => $r['notes'],
                'answered_at'  => $r['answered_at'],
            ];
        }

        // converti mappe associative in liste ordinate
        $functions = [];
        foreach ($byFunc as $f) {
            $cats = [];
            foreach ($f['categories'] as $c) {
                $c['measures'] = array_values($c['measures']);
                $cats[] = $c;
            }
            $f['categories'] = $cats;
            $functions[] = $f;
        }

        $this->jsonSuccess([
            'assessment'   => ['id' => $id, 'entity_level' => $a['entity_level'], 'status' => $a['status']],
            'functions'    => $functions,
            'stats'        => $this->computeStats($id),
        ]);
    }

    /**
     * POST /api/acn-gap/{id}/respond
     * Body: { requirement_key, response_value, evidence?, notes? }
     *   oppure { responses: [ {requirement_key, response_value, ...}, ... ] }
     */
    public function saveResponse(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager', 'auditor']);
        $a = $this->loadOwned($id);
        if ($a['status'] === 'completed') {
            $this->jsonError('Assessment gia completato: non modificabile.', 409, 'ALREADY_COMPLETED');
        }
        $userId = $this->getCurrentUserId();
        $orgId  = $this->getCurrentOrgId();

        $body = $this->getJsonBody();
        $batch = is_array($body['responses'] ?? null) ? $body['responses'] : [$body];

        $valid = ['not_implemented', 'partial', 'implemented', 'not_applicable'];
        $saved = 0;
        foreach ($batch as $item) {
            if (!is_array($item)) {
                continue;
            }
            $key = trim((string) ($item['requirement_key'] ?? ''));
            $val = (string) ($item['response_value'] ?? '');
            if ($key === '' || !in_array($val, $valid, true)) {
                continue;
            }
            $updated = Database::update('acn_assessment_responses', [
                'response_value'      => $val,
                'evidence_description' => isset($item['evidence']) ? (string) $item['evidence'] : null,
                'notes'               => isset($item['notes']) ? (string) $item['notes'] : null,
                'answered_by'         => $userId,
                'answered_at'         => date('Y-m-d H:i:s'),
            ], 'assessment_id = ? AND requirement_key = ? AND organization_id = ?', [$id, $key, $orgId]);
            $saved += $updated > 0 ? 1 : 0;
        }

        // porta a in_progress se era draft
        if ($a['status'] === 'draft' && $saved > 0) {
            Database::update('acn_assessments', ['status' => 'in_progress'], 'id = ?', [$id]);
        }

        $this->jsonSuccess(['saved' => $saved, 'stats' => $this->computeStats($id)], 'Risposte salvate');
    }

    /**
     * POST /api/acn-gap/{id}/complete
     * Calcola overall_score + function_scores + stats e marca completed.
     * Richiede che tutti i requisiti applicabili abbiano una risposta.
     */
    public function complete(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $a = $this->loadOwned($id);
        if ($a['status'] === 'completed') {
            $this->jsonError('Assessment gia completato.', 409, 'ALREADY_COMPLETED');
        }

        $unanswered = (int) (Database::fetchOne(
            'SELECT COUNT(*) AS n FROM acn_assessment_responses WHERE assessment_id = ? AND response_value IS NULL',
            [$id]
        )['n'] ?? 0);
        if ($unanswered > 0) {
            $this->jsonError(
                "Mancano $unanswered requisiti senza risposta. Completa tutte le risposte prima di chiudere.",
                422,
                'INCOMPLETE',
                ['unanswered' => $unanswered]
            );
        }

        [$overall, $funcScores, $stats] = $this->calculateScores($id);

        Database::update('acn_assessments', [
            'status'          => 'completed',
            'overall_score'   => $overall,
            'function_scores' => json_encode($funcScores, JSON_UNESCAPED_UNICODE),
            'stats'           => json_encode($stats, JSON_UNESCAPED_UNICODE),
            'completed_by'    => $this->getCurrentUserId(),
            'completed_at'    => date('Y-m-d H:i:s'),
        ], 'id = ?', [$id]);

        $this->logAudit('acn_assessment_completed', 'acn_assessment', $id, [
            'overall_score' => $overall,
            'entity_level'  => $a['entity_level'],
        ]);

        $this->jsonSuccess([
            'overall_score'   => $overall,
            'function_scores' => $funcScores,
            'stats'           => $stats,
        ], 'Assessment ACN completato');
    }

    /** GET /api/acn-gap/{id}/report */
    public function getReport(int $id): void
    {
        $this->requireOrgAccess();
        $a = $this->loadOwned($id);

        // requisiti non conformi (priorita per il piano d'azione)
        $gaps = Database::fetchAll(
            "SELECT requirement_key, measure_code, function_code, category_code, requirement_text,
                    response_value, notes
               FROM acn_assessment_responses
              WHERE assessment_id = ? AND response_value IN ('not_implemented','partial')
              ORDER BY FIELD(response_value,'not_implemented','partial'), function_code, measure_code, requirement_index",
            [$id]
        );

        $a['function_scores'] = $a['function_scores'] ? json_decode($a['function_scores'], true) : null;
        $a['stats'] = $a['stats'] ? json_decode($a['stats'], true) : $this->computeStats($id);
        $a['ai_recommendations'] = $a['ai_recommendations'] ? json_decode($a['ai_recommendations'], true) : null;

        $this->jsonSuccess(['assessment' => $a, 'gaps' => $gaps]);
    }

    /**
     * POST /api/acn-gap/{id}/aiAnalyze
     * Analisi AI dei gap con grounding sui requisiti ACN (la KB contiene gia i
     * 203 requisiti, scope SYSTEM). Salva sintesi e raccomandazioni.
     */
    public function aiAnalyze(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $a = $this->loadOwned($id);

        $gaps = Database::fetchAll(
            "SELECT measure_code, requirement_index, requirement_text, response_value
               FROM acn_assessment_responses
              WHERE assessment_id = ? AND response_value IN ('not_implemented','partial')
              ORDER BY function_code, measure_code, requirement_index
              LIMIT 60",
            [$id]
        );

        if (empty($gaps)) {
            $this->jsonSuccess(['ai_summary' => 'Nessun gap rilevato: tutti i requisiti applicabili risultano conformi.'], 'Analisi completata');
        }

        require_once APP_PATH . '/services/AIService.php';
        $ai = new AIService();

        $lines = array_map(
            fn($g) => "- {$g['measure_code']} punto {$g['requirement_index']} [{$g['response_value']}]: {$g['requirement_text']}",
            $gaps
        );
        $question = "Analizza i seguenti gap di conformita rispetto alle misure di sicurezza di base ACN "
            . "(Determinazione 164179/2025) per un soggetto " . ($a['entity_level'] === 'essential' ? 'ESSENZIALE' : 'IMPORTANTE') . ". "
            . "Per ciascun gap suggerisci un'azione concreta e prioritizza. Cita SOLO requisiti ACN reali.\n\n"
            . implode("\n", $lines);

        try {
            $userContext = [
                'organization_id'    => $this->getCurrentOrgId(),
                'consulting_firm_id' => $this->currentUser['consulting_firm_id'] ?? null,
            ];
            // askWithRag ritorna ['answer'=>string, 'sources'=>array, 'rag_used'=>bool]:
            // estraiamo il testo, NON salviamo l'intero array.
            $result  = $ai->askWithRag($question, $userContext);
            $answer  = is_array($result) ? (string) ($result['answer'] ?? '') : (string) $result;
            $sources = (is_array($result) && isset($result['sources'])) ? $result['sources'] : [];
        } catch (Throwable $e) {
            error_log('[AcnAssessment] aiAnalyze fallita: ' . $e->getMessage());
            $this->jsonError('Analisi AI temporaneamente non disponibile. Riprova piu tardi.', 503, 'AI_UNAVAILABLE');
        }

        Database::update('acn_assessments', [
            'ai_summary'         => $answer,
            'ai_recommendations' => json_encode($sources, JSON_UNESCAPED_UNICODE),
        ], 'id = ?', [$id]);

        $this->logAudit('acn_assessment_ai_analyzed', 'acn_assessment', $id, ['gaps' => count($gaps)]);
        $this->jsonSuccess(['ai_summary' => $answer, 'sources' => $sources], 'Analisi AI completata');
    }

    // ════════════════════════ HELPER ════════════════════════

    /** Carica l'assessment verificando ownership org (anti-IDOR). */
    private function loadOwned(int $id): array
    {
        $a = Database::fetchOne(
            'SELECT * FROM acn_assessments WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$a) {
            $this->jsonError('Assessment non trovato', 404, 'NOT_FOUND');
        }
        return $a;
    }

    /**
     * Determina il livello del soggetto dall'organization corrente.
     * organizations.entity_type: 'essential'|'important'|'not_applicable'.
     * Ritorna 'essential'|'important', oppure null se non classificato.
     */
    private function resolveEntityLevel(): ?string
    {
        $org = Database::fetchOne('SELECT entity_type FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
        $t = $org['entity_type'] ?? null;
        if ($t === 'essential' || $t === 'important') {
            return $t;
        }
        return null;
    }

    /** Carica e cache il dataset misure ACN. */
    private function measures(): array
    {
        if ($this->measuresCache === null) {
            $path = APP_PATH . '/data/acn_measures.json';
            $json = is_readable($path) ? json_decode((string) file_get_contents($path), true) : null;
            $this->measuresCache = is_array($json) ? $json : ['measures' => []];
        }
        return $this->measuresCache;
    }

    /** Totali requisiti per livello (da dataset). */
    private function datasetTotals(): array
    {
        $t = $this->measures()['totals'] ?? [];
        return [
            'important' => [
                'measures'     => $t['measures_importante'] ?? 37,
                'requirements' => $t['requirements_importante'] ?? 87,
            ],
            'essential' => [
                'measures'     => $t['measures_essenziale'] ?? 43,
                'requirements' => $t['requirements_essenziale'] ?? 116,
            ],
        ];
    }

    /**
     * Lista piatta dei requisiti applicabili a un livello.
     * @return array<int,array{key:string,measure_code:string,index:int,function_code:string,category_code:string,text:string}>
     */
    private function applicableRequirements(string $level): array
    {
        $flag = $level === 'essential' ? 'essenziale' : 'importante';
        $out = [];
        foreach ($this->measures()['measures'] ?? [] as $m) {
            $applies = $level === 'essential' ? ($m['applies_essenziale'] ?? false) : ($m['applies_importante'] ?? false);
            if (!$applies) {
                continue;
            }
            foreach ($m['requirements'] ?? [] as $r) {
                if (empty($r[$flag])) {
                    continue; // punto non applicabile a questo livello
                }
                $out[] = [
                    'key'           => $m['code'] . '#' . $r['index'],
                    'measure_code'  => $m['code'],
                    'index'         => (int) $r['index'],
                    'function_code' => $m['function_code'],
                    'category_code' => $m['category_code'],
                    'text'          => $r['text'],
                ];
            }
        }
        return $out;
    }

    /** Requisiti raggruppati per funzione (per il catalogo). */
    private function groupedRequirements(?string $level, array $responses): array
    {
        if ($level === null) {
            return [];
        }
        $reqs = $this->applicableRequirements($level);
        $meta = $this->measureMeta();
        $byFunc = [];
        foreach ($reqs as $r) {
            $fc = $r['function_code'];
            $byFunc[$fc] ??= ['function_code' => $fc, 'function_name' => $meta['functions'][$fc] ?? $fc, 'measures' => []];
            $mc = $r['measure_code'];
            $byFunc[$fc]['measures'][$mc] ??= ['measure_code' => $mc, 'measure_title' => $meta['measures'][$mc] ?? $mc, 'requirements' => []];
            $byFunc[$fc]['measures'][$mc]['requirements'][] = ['index' => $r['index'], 'text' => $r['text']];
        }
        $out = [];
        foreach ($byFunc as $f) {
            $f['measures'] = array_values($f['measures']);
            $out[] = $f;
        }
        return $out;
    }

    /** Mappe codice->titolo per funzioni/categorie/misure (dal dataset). */
    private function measureMeta(): array
    {
        static $meta = null;
        if ($meta !== null) {
            return $meta;
        }
        $functions = [];
        $categories = [];
        $measures = [];
        foreach ($this->measures()['measures'] ?? [] as $m) {
            $functions[$m['function_code']] = $m['function_name'] ?? $m['function_code'];
            $categories[$m['category_code']] = $m['category_title'] ?? $m['category_code'];
            $measures[$m['code']] = $m['title'] ?? $m['code'];
        }
        $meta = ['functions' => $functions, 'categories' => $categories, 'measures' => $measures];
        return $meta;
    }

    /** Conteggi rapidi per stato risposta. */
    private function computeStats(int $id): array
    {
        $rows = Database::fetchAll(
            'SELECT response_value, COUNT(*) AS n FROM acn_assessment_responses WHERE assessment_id = ? GROUP BY response_value',
            [$id]
        );
        $stats = ['applicable' => 0, 'implemented' => 0, 'partial' => 0, 'not_implemented' => 0, 'not_applicable' => 0, 'unanswered' => 0];
        foreach ($rows as $r) {
            $n = (int) $r['n'];
            $stats['applicable'] += $n;
            $v = $r['response_value'];
            if ($v === null) {
                $stats['unanswered'] += $n;
            } elseif (isset($stats[$v])) {
                $stats[$v] += $n;
            }
        }
        return $stats;
    }

    /**
     * Calcolo punteggi: overall + per funzione.
     * Scoring per requisito: implemented=100, partial=50, not_implemented=0.
     * not_applicable e unanswered ESCLUSI dal denominatore (coerente con
     * l'assessment Art.21 esistente).
     * @return array{0:float,1:array,2:array}
     */
    private function calculateScores(int $id): array
    {
        $rows = Database::fetchAll(
            'SELECT function_code, response_value FROM acn_assessment_responses WHERE assessment_id = ?',
            [$id]
        );
        $val = ['implemented' => 100.0, 'partial' => 50.0, 'not_implemented' => 0.0];

        $sum = 0.0; $cnt = 0;
        $byFunc = [];
        foreach ($rows as $r) {
            $v = $r['response_value'];
            if ($v === 'not_applicable' || $v === null || !isset($val[$v])) {
                continue;
            }
            $fc = $r['function_code'];
            $byFunc[$fc] ??= ['sum' => 0.0, 'cnt' => 0];
            $byFunc[$fc]['sum'] += $val[$v];
            $byFunc[$fc]['cnt']++;
            $sum += $val[$v];
            $cnt++;
        }

        // Se nessun requisito e applicabile (tutti not_applicable) il punteggio
        // non e definito: null, non 0.0 (che significherebbe "0% conforme").
        $overall = $cnt > 0 ? round($sum / $cnt, 2) : null;
        $funcScores = [];
        foreach ($byFunc as $fc => $d) {
            $funcScores[$fc] = $d['cnt'] > 0 ? round($d['sum'] / $d['cnt'], 2) : 0.0;
        }

        return [$overall, $funcScores, $this->computeStats($id)];
    }
}
