<?php
/**
 * NIS2 Agile - Policy Controller
 *
 * Gestione policy e procedure di sicurezza, con AI generation.
 */

require_once __DIR__ . '/BaseController.php';
require_once APP_PATH . '/services/AIService.php';
require_once APP_PATH . '/services/WebhookService.php';

class PolicyController extends BaseController
{
    public function list(): void
    {
        $this->requireOrgAccess();

        $where = 'organization_id = ?';
        $params = [$this->getCurrentOrgId()];

        if ($this->hasParam('status')) {
            $where .= ' AND status = ?';
            $params[] = $this->getParam('status');
        }
        if ($this->hasParam('category')) {
            $where .= ' AND category = ?';
            $params[] = $this->getParam('category');
        }

        $policies = Database::fetchAll(
            "SELECT p.*, u.full_name as approved_by_name
             FROM policies p
             LEFT JOIN users u ON u.id = p.approved_by
             WHERE p.{$where}
             ORDER BY p.category, p.title",
            $params
        );

        $this->jsonSuccess($policies);
    }

    public function create(): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $this->validateRequired(['title', 'category']);

        $policyId = Database::insert('policies', [
            'organization_id' => $this->getCurrentOrgId(),
            'title'           => trim($this->getParam('title')),
            'category'        => $this->getParam('category'),
            'nis2_article'    => $this->getParam('nis2_article'),
            'content'         => $this->getParam('content'),
            'next_review_date' => $this->getParam('next_review_date'),
            'ai_generated'    => $this->getParam('ai_generated', 0),
        ]);

        $this->logAudit('policy_created', 'policy', $policyId);
        $this->jsonSuccess(['id' => $policyId], 'Policy creata', 201);
    }

    public function get(int $id): void
    {
        $this->requireOrgAccess();

        $policy = Database::fetchOne(
            'SELECT p.*, u.full_name as approved_by_name
             FROM policies p
             LEFT JOIN users u ON u.id = p.approved_by
             WHERE p.id = ? AND p.organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );

        if (!$policy) {
            $this->jsonError('Policy non trovata', 404, 'POLICY_NOT_FOUND');
        }

        $this->jsonSuccess($policy);
    }

    public function update(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);

        $updates = [];
        foreach (['title', 'content', 'category', 'nis2_article', 'status', 'version', 'next_review_date'] as $field) {
            if ($this->hasParam($field)) {
                $updates[$field] = $this->getParam($field);
            }
        }

        if (!empty($updates)) {
            Database::update('policies', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
            $this->logAudit('policy_updated', 'policy', $id, $updates);
        }

        $this->jsonSuccess($updates, 'Policy aggiornata');
    }

    public function delete(int $id): void
    {
        $this->requireOrgRole(['org_admin']);

        $deleted = Database::delete('policies', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
        if ($deleted === 0) {
            $this->jsonError('Policy non trovata', 404, 'POLICY_NOT_FOUND');
        }

        $this->logAudit('policy_deleted', 'policy', $id);
        $this->jsonSuccess(null, 'Policy eliminata');
    }

    public function approve(int $id): void
    {
        $this->requireOrgRole(['org_admin']);

        Database::update('policies', [
            'status'      => 'approved',
            'approved_by' => $this->getCurrentUserId(),
            'approved_at' => date('Y-m-d H:i:s'),
        ], 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);

        $this->logAudit('policy_approved', 'policy', $id);

        $policy = Database::fetchOne('SELECT * FROM policies WHERE id = ?', [$id]);

        // Snapshot versione per storico/diff/attestation (P3).
        // Idempotente sulla coppia (policy_id, version): la riapprovazione della
        // stessa versione aggiorna lo snapshot invece di duplicarlo (UNIQUE uq_policy_version).
        try {
            Database::query(
                'INSERT INTO policy_versions (policy_id, organization_id, version, content, change_note, created_by)
                 VALUES (?, ?, ?, ?, ?, ?)
                 ON DUPLICATE KEY UPDATE content = VALUES(content), change_note = VALUES(change_note),
                                         created_by = VALUES(created_by), created_at = NOW()',
                [
                    $id,
                    $this->getCurrentOrgId(),
                    $policy['version'] ?? '1.0',
                    $policy['content'] ?? null,
                    $this->getParam('change_note') ?: 'Approvazione',
                    $this->getCurrentUserId(),
                ]
            );
        } catch (Throwable $e) {
            error_log('[POLICY_VER] ' . $e->getMessage());
        }

        // Dispatch webhook policy.approved
        try {
            (new WebhookService())->dispatch(
                $this->getCurrentOrgId(),
                'policy.approved',
                WebhookService::policyPayload($policy)
            );
        } catch (Throwable $e) {
            error_log('[WEBHOOK] dispatch error: ' . $e->getMessage());
        }

        $this->jsonSuccess(null, 'Policy approvata');
    }

    public function aiGeneratePolicy(): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $this->validateRequired(['category']);

        $category = $this->getParam('category');
        $org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);

        try {
            $aiService = new AIService();
            $generated = $aiService->generatePolicy($category, $org);

            $aiService->logInteraction(
                $this->getCurrentOrgId(),
                $this->getCurrentUserId(),
                'policy_draft',
                "Generate {$category} policy",
                substr($generated['title'] ?? '', 0, 500)
            );

            $this->jsonSuccess($generated, 'Policy generata dall\'AI');
        } catch (Throwable $e) {
            $this->jsonError('Errore AI: ' . $e->getMessage(), 500, 'AI_ERROR');
        }
    }

    public function getTemplates(): void
    {
        $this->requireOrgAccess();

        $templates = [
            ['category' => 'information_security', 'title' => 'Politica di Sicurezza delle Informazioni', 'nis2_article' => '21.2.a'],
            ['category' => 'access_control', 'title' => 'Politica di Controllo degli Accessi', 'nis2_article' => '21.2.i'],
            ['category' => 'incident_response', 'title' => 'Piano di Risposta agli Incidenti', 'nis2_article' => '21.2.b'],
            ['category' => 'business_continuity', 'title' => 'Piano di Continuità Operativa', 'nis2_article' => '21.2.c'],
            ['category' => 'supply_chain', 'title' => 'Politica di Sicurezza della Supply Chain', 'nis2_article' => '21.2.d'],
            ['category' => 'encryption', 'title' => 'Politica sulla Crittografia', 'nis2_article' => '21.2.h'],
            ['category' => 'hr_security', 'title' => 'Politica di Sicurezza delle Risorse Umane', 'nis2_article' => '21.2.i'],
            ['category' => 'asset_management', 'title' => 'Politica di Gestione degli Asset', 'nis2_article' => '21.2.i'],
            ['category' => 'network_security', 'title' => 'Politica di Sicurezza della Rete', 'nis2_article' => '21.2.e'],
            ['category' => 'vulnerability_management', 'title' => 'Politica di Gestione delle Vulnerabilità', 'nis2_article' => '21.2.e'],
        ];

        $this->jsonSuccess($templates);
    }

    // ══════════════════════════════════════════════════════════════════════
    // ATTESTATION + VERSIONING (P3)
    // ══════════════════════════════════════════════════════════════════════

    /** POST /api/policies/{id}/attest — dipendente prende visione (versione corrente). */
    public function attest(int $id): void
    {
        $this->requireOrgAccess();
        $policy = Database::fetchOne(
            "SELECT id, version, status FROM policies WHERE id = ? AND organization_id = ? AND deleted_at IS NULL",
            [$id, $this->getCurrentOrgId()]
        );
        if (!$policy) {
            $this->jsonError('Policy non trovata', 404, 'NOT_FOUND');
        }
        if ($policy['status'] !== 'approved') {
            $this->jsonError('Si puo attestare solo una policy approvata', 422, 'NOT_APPROVED');
        }
        $version = $policy['version'] ?? '1.0';

        $existing = Database::fetchOne(
            'SELECT id FROM policy_attestations WHERE policy_id = ? AND user_id = ? AND version = ?',
            [$id, $this->getCurrentUserId(), $version]
        );
        if ($existing) {
            $this->jsonSuccess(['already' => true, 'version' => $version], 'Presa visione gia registrata');
            return;
        }

        Database::insert('policy_attestations', [
            'policy_id'       => $id,
            'organization_id' => $this->getCurrentOrgId(),
            'user_id'         => $this->getCurrentUserId(),
            'version'         => $version,
            'ip_address'     => $this->attClientIp(),
        ]);
        $this->logAudit('policy_attested', 'policy', $id, ['version' => $version]);
        $this->jsonSuccess(['version' => $version], 'Presa visione registrata', 201);
    }

    /** GET /api/policies/{id}/attestations — copertura % presa visione (versione corrente). */
    public function attestations(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member', 'auditor']);
        $policy = Database::fetchOne(
            "SELECT id, title, version FROM policies WHERE id = ? AND organization_id = ? AND deleted_at IS NULL",
            [$id, $this->getCurrentOrgId()]
        );
        if (!$policy) {
            $this->jsonError('Policy non trovata', 404, 'NOT_FOUND');
        }
        $version = $policy['version'] ?? '1.0';

        $members = Database::fetchAll(
            'SELECT u.id, u.full_name, u.email FROM user_organizations uo
             JOIN users u ON u.id = uo.user_id
             WHERE uo.organization_id = ? AND u.is_active = 1',
            [$this->getCurrentOrgId()]
        );
        $attested = Database::fetchAll(
            'SELECT user_id, attested_at FROM policy_attestations WHERE policy_id = ? AND version = ?',
            [$id, $version]
        );
        $attMap = [];
        foreach ($attested as $a) { $attMap[(int) $a['user_id']] = $a['attested_at']; }

        $list = array_map(fn($m) => [
            'user_id'     => (int) $m['id'],
            'full_name'   => $m['full_name'],
            'email'       => $m['email'],
            'attested'    => isset($attMap[(int) $m['id']]),
            'attested_at' => $attMap[(int) $m['id']] ?? null,
        ], $members);

        $total = count($members);
        $done = count(array_filter($list, fn($x) => $x['attested']));
        $this->jsonSuccess([
            'policy_id'        => $id,
            'version'          => $version,
            'total_members'    => $total,
            'attested'         => $done,
            'coverage_percent' => $total ? (int) round($done * 100 / $total) : 0,
            'members'          => $list,
        ]);
    }

    /** GET /api/policies/{id}/versions — storico versioni. */
    public function versions(int $id): void
    {
        $this->requireOrgAccess();
        $rows = Database::fetchAll(
            'SELECT id, version, change_note, created_by, created_at FROM policy_versions
             WHERE policy_id = ? AND organization_id = ? ORDER BY created_at DESC',
            [$id, $this->getCurrentOrgId()]
        );
        $this->jsonSuccess(['versions' => $rows]);
    }

    /** GET /api/policies/{id}/diff?from={vId}&to={vId} — diff line-by-line. */
    public function diff(int $id): void
    {
        $this->requireOrgAccess();
        $fromId = (int) $this->getParam('from');
        $toId   = (int) $this->getParam('to');
        $a = Database::fetchOne('SELECT version, content FROM policy_versions WHERE id = ? AND policy_id = ? AND organization_id = ?', [$fromId, $id, $this->getCurrentOrgId()]);
        $b = Database::fetchOne('SELECT version, content FROM policy_versions WHERE id = ? AND policy_id = ? AND organization_id = ?', [$toId, $id, $this->getCurrentOrgId()]);
        if (!$a || !$b) {
            $this->jsonError('Versione non trovata', 404, 'NOT_FOUND');
        }
        $linesA = preg_split('/\r\n|\r|\n/', (string) $a['content']);
        $linesB = preg_split('/\r\n|\r|\n/', (string) $b['content']);
        $d = self::lcsLineDiff($linesA, $linesB);
        $this->jsonSuccess([
            'from'     => ['id' => $fromId, 'version' => $a['version']],
            'to'       => ['id' => $toId,   'version' => $b['version']],
            'added'    => $d['added'],
            'removed'  => $d['removed'],
            'summary'  => ['added' => count($d['added']), 'removed' => count($d['removed'])],
        ]);
    }

    /**
     * Diff line-by-line basato su LCS (Longest Common Subsequence).
     * Rileva correttamente righe duplicate, spostamenti e modifiche posizionali,
     * a differenza di un confronto set-based. Restituisce le righe aggiunte (in B
     * ma non allineate in A) e rimosse (in A ma non allineate in B), con posizione.
     *
     * @return array{added: list<array{line:int,text:string}>, removed: list<array{line:int,text:string}>}
     */
    private static function lcsLineDiff(array $a, array $b): array
    {
        $n = count($a);
        $m = count($b);
        // Tabella LCS (lunghezze). Cap di sicurezza per evitare blow-up su documenti enormi.
        if ($n > 4000 || $m > 4000) {
            // fallback prudente: confronto posizionale semplice riga per riga
            $added = []; $removed = [];
            $max = max($n, $m);
            for ($i = 0; $i < $max; $i++) {
                $la = $a[$i] ?? null; $lb = $b[$i] ?? null;
                if ($la !== $lb) {
                    if ($lb !== null && trim($lb) !== '') $added[] = ['line' => $i + 1, 'text' => $lb];
                    if ($la !== null && trim($la) !== '') $removed[] = ['line' => $i + 1, 'text' => $la];
                }
            }
            return ['added' => $added, 'removed' => $removed];
        }

        $dp = array_fill(0, $n + 1, array_fill(0, $m + 1, 0));
        for ($i = $n - 1; $i >= 0; $i--) {
            for ($j = $m - 1; $j >= 0; $j--) {
                $dp[$i][$j] = ($a[$i] === $b[$j])
                    ? $dp[$i + 1][$j + 1] + 1
                    : max($dp[$i + 1][$j], $dp[$i][$j + 1]);
            }
        }
        $added = []; $removed = [];
        $i = 0; $j = 0;
        while ($i < $n && $j < $m) {
            if ($a[$i] === $b[$j]) {
                $i++; $j++;
            } elseif ($dp[$i + 1][$j] >= $dp[$i][$j + 1]) {
                if (trim($a[$i]) !== '') $removed[] = ['line' => $i + 1, 'text' => $a[$i]];
                $i++;
            } else {
                if (trim($b[$j]) !== '') $added[] = ['line' => $j + 1, 'text' => $b[$j]];
                $j++;
            }
        }
        for (; $i < $n; $i++) { if (trim($a[$i]) !== '') $removed[] = ['line' => $i + 1, 'text' => $a[$i]]; }
        for (; $j < $m; $j++) { if (trim($b[$j]) !== '') $added[] = ['line' => $j + 1, 'text' => $b[$j]]; }
        return ['added' => $added, 'removed' => $removed];
    }

    /** GET /api/policies/pending-attestations — policy approvate non ancora attestate dall'utente. */
    public function pendingAttestations(): void
    {
        $this->requireOrgAccess();
        $rows = Database::fetchAll(
            "SELECT p.id, p.title, p.category, p.version, p.approved_at
             FROM policies p
             WHERE p.organization_id = ? AND p.status = 'approved' AND p.deleted_at IS NULL
               AND NOT EXISTS (
                   SELECT 1 FROM policy_attestations a
                   WHERE a.policy_id = p.id AND a.user_id = ? AND a.version = p.version
               )
             ORDER BY p.approved_at DESC",
            [$this->getCurrentOrgId(), $this->getCurrentUserId()]
        );
        $this->jsonSuccess(['pending' => $rows, 'count' => count($rows)]);
    }

    private function attClientIp(): ?string
    {
        $fwd = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
        if ($fwd) { return trim(explode(',', $fwd)[0]); }
        return $_SERVER['REMOTE_ADDR'] ?? null;
    }
}
