<?php
/**
 * NIS2 Agile - Supply Chain Controller
 *
 * Gestione fornitori, assessment cybersecurity, risk scoring.
 */

require_once __DIR__ . '/BaseController.php';

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

        $suppliers = Database::fetchAll(
            'SELECT * FROM suppliers WHERE organization_id = ? ORDER BY criticality DESC, name',
            [$this->getCurrentOrgId()]
        );

        $this->jsonSuccess($suppliers);
    }

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

        $supplierId = Database::insert('suppliers', [
            'organization_id'     => $this->getCurrentOrgId(),
            'name'                => trim($this->getParam('name')),
            'vat_number'          => $this->getParam('vat_number'),
            'contact_email'       => $this->getParam('contact_email'),
            'contact_name'        => $this->getParam('contact_name'),
            'service_type'        => $this->getParam('service_type'),
            'service_description' => $this->getParam('service_description'),
            'criticality'         => $this->getParam('criticality', 'medium'),
            'category_id'         => $this->getParam('category_id') ?: null,
            'contract_start_date' => $this->getParam('contract_start_date'),
            'contract_expiry_date' => $this->getParam('contract_expiry_date'),
            'notes'               => $this->getParam('notes'),
        ]);

        $this->logAudit('supplier_created', 'supplier', $supplierId);
        $this->jsonSuccess(['id' => $supplierId], 'Fornitore aggiunto', 201);
    }

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

        $supplier = Database::fetchOne(
            'SELECT * FROM suppliers WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );

        if (!$supplier) {
            $this->jsonError('Fornitore non trovato', 404, 'SUPPLIER_NOT_FOUND');
        }

        $this->jsonSuccess($supplier);
    }

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

        $updates = [];
        $fields = ['name', 'vat_number', 'contact_email', 'contact_name', 'service_type',
                    'service_description', 'criticality', 'category_id', 'contract_start_date', 'contract_expiry_date',
                    'security_requirements_met', 'notes', 'status'];

        foreach ($fields as $field) {
            if ($this->hasParam($field)) {
                $updates[$field] = $this->getParam($field);
            }
        }

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

        $this->jsonSuccess($updates, 'Fornitore aggiornato');
    }

    public function delete(int $id): void
    {
        $this->requireOrgRole(['org_admin']);
        $deleted = Database::delete('suppliers', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
        if ($deleted === 0) {
            $this->jsonError('Fornitore non trovato', 404, 'SUPPLIER_NOT_FOUND');
        }
        $this->logAudit('supplier_deleted', 'supplier', $id);
        $this->jsonSuccess(null, 'Fornitore eliminato');
    }

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

        $responses = $this->getParam('assessment_responses');
        $riskScore = $this->calculateSupplierRiskScore($responses);

        Database::update('suppliers', [
            'assessment_responses'  => json_encode($responses),
            'risk_score'            => $riskScore,
            'last_assessment_date'  => date('Y-m-d'),
            'next_assessment_date'  => date('Y-m-d', strtotime('+6 months')),
            'security_requirements_met' => $riskScore >= 70 ? 1 : 0,
        ], 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);

        $this->logAudit('supplier_assessed', 'supplier', $id, ['risk_score' => $riskScore]);
        $this->jsonSuccess(['risk_score' => $riskScore], 'Assessment fornitore completato');
    }

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

        $overview = Database::fetchAll(
            'SELECT criticality, status,
                    COUNT(*) as count,
                    AVG(risk_score) as avg_risk_score,
                    SUM(CASE WHEN security_requirements_met = 0 THEN 1 ELSE 0 END) as non_compliant
             FROM suppliers
             WHERE organization_id = ?
             GROUP BY criticality, status',
            [$this->getCurrentOrgId()]
        );

        $expiring = Database::fetchAll(
            'SELECT id, name, contract_expiry_date, criticality
             FROM suppliers
             WHERE organization_id = ? AND contract_expiry_date IS NOT NULL
                   AND contract_expiry_date <= DATE_ADD(NOW(), INTERVAL 90 DAY)
                   AND status = "active"
             ORDER BY contract_expiry_date',
            [$this->getCurrentOrgId()]
        );

        $this->jsonSuccess([
            'overview'  => $overview,
            'expiring_contracts' => $expiring,
        ]);
    }

    private function calculateSupplierRiskScore(array $responses): int
    {
        if (empty($responses)) return 0;

        $totalScore = 0;
        $totalWeight = 0;

        foreach ($responses as $resp) {
            $weight = $resp['weight'] ?? 1;
            $value = match ($resp['value'] ?? '') {
                'yes', 'implemented' => 100,
                'partial'            => 50,
                default              => 0,
            };
            $totalScore += $value * $weight;
            $totalWeight += 100 * $weight;
        }

        return $totalWeight > 0 ? (int) round($totalScore / $totalWeight * 100) : 0;
    }

    // ══════════════════════════════════════════════════════════════════════
    // SELF-ASSESSMENT FORNITORI (P3) — questionario sicurezza Art.21.2.d
    // ══════════════════════════════════════════════════════════════════════

    private const QUESTIONNAIRE = [
        ['key' => 'iso27001',      'q' => 'Disponete di una certificazione ISO/IEC 27001 valida?', 'weight' => 15],
        ['key' => 'mfa',           'q' => 'Imponete autenticazione a piu fattori (MFA) per gli accessi ai sistemi che trattano dati del cliente?', 'weight' => 15],
        ['key' => 'patching',      'q' => 'Avete un processo formale di patch management e gestione vulnerabilita?', 'weight' => 12],
        ['key' => 'backup',        'q' => 'Eseguite backup regolari con test di ripristino documentati?', 'weight' => 12],
        ['key' => 'incident',      'q' => 'Disponete di un piano di gestione degli incidenti con notifica al cliente?', 'weight' => 14],
        ['key' => 'access_review',  'q' => 'Effettuate revisioni periodiche degli accessi e revoca tempestiva?', 'weight' => 10],
        ['key' => 'encryption',    'q' => 'I dati del cliente sono cifrati a riposo e in transito?', 'weight' => 12],
        ['key' => 'subcontractor', 'q' => 'Valutate la sicurezza dei vostri sub-fornitori (quarta parte)?', 'weight' => 10],
    ];

    /** POST /api/supply-chain/{id}/send-questionnaire (JWT): genera token + link pubblico. */
    public function sendQuestionnaire(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $supplier = Database::fetchOne(
            'SELECT id, name, contact_email FROM suppliers WHERE id = ? AND organization_id = ? AND deleted_at IS NULL',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$supplier) {
            $this->jsonError('Fornitore non trovato', 404, 'NOT_FOUND');
        }

        $rawToken = 'sq_' . bin2hex(random_bytes(20));
        $email    = $this->getParam('email') ?: $supplier['contact_email'];
        $expires  = date('Y-m-d H:i:s', time() + 30 * 86400);

        Database::insert('supplier_questionnaires', [
            'organization_id' => $this->getCurrentOrgId(),
            'supplier_id'     => $id,
            'token_hash'      => hash('sha256', $rawToken),
            'status'          => 'sent',
            'sent_to_email'   => $email,
            'expires_at'      => $expires,
        ]);

        $link = 'https://nis2.agile.software/supplier-assessment.html?token=' . $rawToken;

        try {
            if ($email && class_exists('EmailService')) {
                $svc = new EmailService();
                if (method_exists($svc, 'send')) {
                    $svc->send($email, 'Questionario di sicurezza fornitore - NIS2',
                        "<p>Gentile {$supplier['name']},</p><p>vi chiediamo di compilare il questionario di sicurezza al seguente link (valido 30 giorni):</p><p><a href=\"{$link}\">{$link}</a></p>");
                }
            }
        } catch (Throwable $e) {
            error_log('[SUPPLIER_Q] email: ' . $e->getMessage());
        }

        $this->logAudit('supplier_questionnaire_sent', 'supplier', $id, ['email' => $email]);
        $this->jsonSuccess(['supplier_id' => $id, 'sent_to' => $email, 'link' => $link, 'expires_at' => $expires], 'Questionario generato', 201);
    }

    /** GET /api/supply-chain/public-questionnaire?token=xxx (NO AUTH). */
    public function publicQuestionnaire(): void
    {
        $q = $this->resolveQuestionnaire($_GET['token'] ?? '');
        if ($q['status'] === 'completed') {
            $this->jsonError('Questionario gia compilato', 409, 'ALREADY_COMPLETED');
        }
        $supplier = Database::fetchOne('SELECT name FROM suppliers WHERE id = ?', [$q['supplier_id']]);
        $this->jsonSuccess([
            'supplier_name' => $supplier['name'] ?? 'Fornitore',
            'questions'     => array_map(fn($x) => ['key' => $x['key'], 'question' => $x['q']], self::QUESTIONNAIRE),
            'answer_scale'  => ['yes' => 'Si', 'partial' => 'Parzialmente', 'no' => 'No'],
        ]);
    }

    /** POST /api/supply-chain/submit-public-questionnaire (NO AUTH). Body: {token, answers}. */
    public function submitPublicQuestionnaire(): void
    {
        $body = $this->getJsonBody();
        $q = $this->resolveQuestionnaire($body['token'] ?? '');
        if ($q['status'] === 'completed') {
            $this->jsonError('Questionario gia compilato', 409, 'ALREADY_COMPLETED');
        }

        $answers = is_array($body['answers'] ?? null) ? $body['answers'] : [];
        $scoreMap = ['yes' => 1.0, 'partial' => 0.5, 'no' => 0.0];
        $earned = 0; $maxScore = 0; $clean = [];
        foreach (self::QUESTIONNAIRE as $item) {
            $maxScore += $item['weight'];
            $a = strtolower((string) ($answers[$item['key']] ?? 'no'));
            if (!isset($scoreMap[$a])) $a = 'no';
            $clean[$item['key']] = $a;
            $earned += $item['weight'] * $scoreMap[$a];
        }
        $score = $maxScore > 0 ? (int) round($earned / $maxScore * 100) : 0;
        $riskLevel = $score >= 80 ? 'low' : ($score >= 60 ? 'medium' : ($score >= 40 ? 'high' : 'critical'));

        // Completamento ATOMICO: vincola lo UPDATE a status='sent' così due submit
        // concorrenti con lo stesso token non possono completare due volte (la seconda
        // tocca 0 righe -> 409). Previene il doppio-submit senza transazione esplicita.
        $upd = Database::query(
            'UPDATE supplier_questionnaires SET status=?, answers=?, score=?, risk_level=?, completed_at=NOW()
             WHERE id=? AND status=?',
            ['completed', json_encode($clean, JSON_UNESCAPED_UNICODE), $score, $riskLevel, $q['id'], 'sent']
        );
        if ($upd->rowCount() === 0) {
            $this->jsonError('Questionario gia compilato', 409, 'ALREADY_COMPLETED');
        }

        // Coerenza con assessSupplier: suppliers.risk_score = punteggio di COMPLIANCE
        // (alto = buono), e security_requirements_met soglia 70. NON sovrascriviamo
        // suppliers.criticality (è la criticità del fornitore, non l'esito del questionario).
        Database::query(
            'UPDATE suppliers SET risk_score=?, security_requirements_met=?, last_assessment_date=CURDATE()
             WHERE id=? AND organization_id=?',
            [$score, $score >= 70 ? 1 : 0, $q['supplier_id'], $q['organization_id']]
        );

        $this->jsonSuccess(['score' => $score, 'risk_level' => $riskLevel], 'Questionario inviato. Grazie.', 201);
    }

    /** GET /api/supply-chain/{id}/questionnaire-status (JWT). */
    public function questionnaireStatus(int $id): void
    {
        $this->requireOrgAccess();
        $rows = Database::fetchAll(
            'SELECT id, status, score, risk_level, sent_to_email, sent_at, completed_at, expires_at
             FROM supplier_questionnaires WHERE supplier_id = ? AND organization_id = ? ORDER BY sent_at DESC',
            [$id, $this->getCurrentOrgId()]
        );
        $this->jsonSuccess(['questionnaires' => $rows]);
    }

    // ══════════════════════════════════════════════════════════════════════
    // MODULO QUESTIONARI CONFIGURABILE (Fase 1) — categorie, template, domande
    // ══════════════════════════════════════════════════════════════════════

    /** GET /api/supply-chain/categories — preset di sistema (org 0) + categorie dell'org. */
    public function categories(): void
    {
        $this->requireOrgAccess();
        $rows = Database::fetchAll(
            'SELECT id, organization_id, slug, name, description, is_system
               FROM supplier_categories
              WHERE organization_id = 0 OR organization_id = ?
              ORDER BY is_system DESC, name',
            [$this->getCurrentOrgId()]
        );
        $this->jsonSuccess($rows);
    }

    /** POST /api/supply-chain/categories — crea categoria custom per l'org. */
    public function createCategory(): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $this->validateRequired(['name']);

        $name = trim((string) $this->getParam('name'));
        $slug = $this->slugify($this->getParam('slug') ?: $name);
        if ($slug === '') {
            $this->jsonError('Nome categoria non valido', 422, 'INVALID_NAME');
        }

        // Nessun conflitto con slug esistente (preset o della stessa org)
        $exists = Database::fetchOne(
            'SELECT id FROM supplier_categories WHERE slug = ? AND organization_id IN (0, ?)',
            [$slug, $this->getCurrentOrgId()]
        );
        if ($exists) {
            $this->jsonError('Esiste gia una categoria con questo identificativo', 409, 'DUPLICATE_SLUG');
        }

        $id = Database::insert('supplier_categories', [
            'organization_id' => $this->getCurrentOrgId(),
            'slug'            => $slug,
            'name'            => $name,
            'description'     => $this->getParam('description'),
            'is_system'       => 0,
        ]);
        $this->logAudit('supplier_category_created', 'supplier_category', $id);
        $this->jsonSuccess(['id' => $id], 'Categoria creata', 201);
    }

    /** PUT /api/supply-chain/categories/{id} — modifica categoria custom (no preset). */
    public function updateCategory(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $cat = Database::fetchOne(
            'SELECT id FROM supplier_categories WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$cat) {
            $this->jsonError('Categoria non trovata o non modificabile', 404, 'NOT_FOUND');
        }
        $updates = [];
        foreach (['name', 'description'] as $f) {
            if ($this->hasParam($f)) $updates[$f] = $this->getParam($f);
        }
        if (!empty($updates)) {
            Database::update('supplier_categories', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
            $this->logAudit('supplier_category_updated', 'supplier_category', $id, $updates);
        }
        $this->jsonSuccess($updates, 'Categoria aggiornata');
    }

    /** DELETE /api/supply-chain/categories/{id} — elimina categoria custom (no preset, no se in uso). */
    public function deleteCategory(int $id): void
    {
        $this->requireOrgRole(['org_admin']);
        $cat = Database::fetchOne(
            'SELECT id FROM supplier_categories WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$cat) {
            $this->jsonError('Categoria non trovata o non eliminabile', 404, 'NOT_FOUND');
        }
        $inUse = Database::fetchOne(
            'SELECT COUNT(*) AS n FROM suppliers WHERE category_id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if ((int) ($inUse['n'] ?? 0) > 0) {
            $this->jsonError('Categoria in uso da uno o piu fornitori', 409, 'CATEGORY_IN_USE');
        }
        Database::delete('supplier_categories', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
        $this->logAudit('supplier_category_deleted', 'supplier_category', $id);
        $this->jsonSuccess(null, 'Categoria eliminata');
    }

    /** GET /api/supply-chain/templates — lista template dell'org (+ conteggio domande). */
    public function templates(): void
    {
        $this->requireOrgAccess();
        $rows = Database::fetchAll(
            'SELECT t.id, t.name, t.description, t.category_id, t.current_version, t.status,
                    t.is_default, t.pass_threshold, c.name AS category_name,
                    (SELECT COUNT(*) FROM questionnaire_questions q WHERE q.template_id = t.id) AS question_count
               FROM questionnaire_templates t
               LEFT JOIN supplier_categories c ON c.id = t.category_id
              WHERE t.organization_id = ?
              ORDER BY t.is_default DESC, t.name',
            [$this->getCurrentOrgId()]
        );
        $this->jsonSuccess($rows);
    }

    /** GET /api/supply-chain/templates/{id} — template + domande ordinate. */
    public function getTemplate(int $id): void
    {
        $this->requireOrgAccess();
        $tpl = Database::fetchOne(
            'SELECT * FROM questionnaire_templates WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$tpl) {
            $this->jsonError('Template non trovato', 404, 'NOT_FOUND');
        }
        $tpl['questions'] = Database::fetchAll(
            'SELECT id, question_code, question_text, question_type, options, weight,
                    is_required, order_index, nis2_ref, vuln_flag, high_criticality_only
               FROM questionnaire_questions WHERE template_id = ? ORDER BY order_index, id',
            [$id]
        );
        $this->jsonSuccess($tpl);
    }

    /** POST /api/supply-chain/templates — crea template (vuoto o da scope). */
    public function createTemplate(): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $this->validateRequired(['name']);

        $categoryId = $this->getParam('category_id');
        if ($categoryId !== null && $categoryId !== '') {
            $this->assertCategoryVisible((int) $categoryId);
        }

        $id = Database::insert('questionnaire_templates', [
            'organization_id' => $this->getCurrentOrgId(),
            'category_id'     => ($categoryId !== null && $categoryId !== '') ? (int) $categoryId : null,
            'name'            => trim((string) $this->getParam('name')),
            'description'     => $this->getParam('description'),
            'current_version' => '1.0',
            'status'          => 'draft',
            'pass_threshold'  => $this->getParam('pass_threshold') !== null ? (int) $this->getParam('pass_threshold') : null,
            'is_default'      => $this->getParam('is_default') ? 1 : 0,
            'created_by'      => $this->getCurrentUserId(),
        ]);
        $this->logAudit('questionnaire_template_created', 'questionnaire_template', $id);
        $this->jsonSuccess(['id' => $id], 'Template creato', 201);
    }

    /** PUT /api/supply-chain/templates/{id} — aggiorna metadati template. */
    public function updateTemplate(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $tpl = Database::fetchOne(
            'SELECT id FROM questionnaire_templates WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$tpl) {
            $this->jsonError('Template non trovato', 404, 'NOT_FOUND');
        }
        $updates = [];
        foreach (['name', 'description', 'status'] as $f) {
            if ($this->hasParam($f)) $updates[$f] = $this->getParam($f);
        }
        if ($this->hasParam('category_id')) {
            $cid = $this->getParam('category_id');
            if ($cid !== null && $cid !== '') $this->assertCategoryVisible((int) $cid);
            $updates['category_id'] = ($cid !== null && $cid !== '') ? (int) $cid : null;
        }
        if ($this->hasParam('pass_threshold')) {
            $updates['pass_threshold'] = $this->getParam('pass_threshold') !== null ? (int) $this->getParam('pass_threshold') : null;
        }
        if ($this->hasParam('is_default')) {
            $updates['is_default'] = $this->getParam('is_default') ? 1 : 0;
        }
        if (!empty($updates)) {
            Database::update('questionnaire_templates', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
            $this->logAudit('questionnaire_template_updated', 'questionnaire_template', $id, $updates);
        }
        $this->jsonSuccess($updates, 'Template aggiornato');
    }

    /** POST /api/supply-chain/templates/{id}/questions — aggiunge una domanda. */
    public function addTemplateQuestion(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $this->assertTemplateOwned($id);
        $this->validateRequired(['question_text']);

        $type = $this->getParam('question_type', 'yes_no_partial');
        $allowed = ['yes_no_partial', 'single_choice', 'multi_choice', 'scale_1_5', 'text', 'number', 'file'];
        if (!in_array($type, $allowed, true)) {
            $this->jsonError('Tipo domanda non valido', 422, 'INVALID_TYPE');
        }

        $options = $this->getParam('options');
        $maxOrder = Database::fetchOne('SELECT COALESCE(MAX(order_index),0) AS m FROM questionnaire_questions WHERE template_id = ?', [$id]);

        $qid = Database::insert('questionnaire_questions', [
            'template_id'           => $id,
            'organization_id'       => $this->getCurrentOrgId(),
            'question_code'         => $this->getParam('question_code'),
            'question_text'         => trim((string) $this->getParam('question_text')),
            'question_type'         => $type,
            'options'               => $options !== null ? json_encode($options, JSON_UNESCAPED_UNICODE) : null,
            'weight'                => $this->getParam('weight') !== null ? (float) $this->getParam('weight') : 1.0,
            'is_required'           => $this->getParam('is_required', true) ? 1 : 0,
            'order_index'           => $this->getParam('order_index') !== null ? (int) $this->getParam('order_index') : ((int) ($maxOrder['m'] ?? 0) + 1),
            'nis2_ref'              => $this->getParam('nis2_ref'),
            'vuln_flag'             => $this->getParam('vuln_flag'),
            'high_criticality_only' => $this->getParam('high_criticality_only') ? 1 : 0,
        ]);
        $this->logAudit('questionnaire_question_added', 'questionnaire_template', $id, ['question_id' => $qid]);
        $this->jsonSuccess(['id' => $qid], 'Domanda aggiunta', 201);
    }

    /** PUT /api/supply-chain/questions/{id} — modifica domanda. */
    public function updateTemplateQuestion(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $q = Database::fetchOne(
            'SELECT id FROM questionnaire_questions WHERE id = ? AND organization_id = ?',
            [$id, $this->getCurrentOrgId()]
        );
        if (!$q) {
            $this->jsonError('Domanda non trovata', 404, 'NOT_FOUND');
        }
        $updates = [];
        foreach (['question_code', 'question_text', 'nis2_ref', 'vuln_flag'] as $f) {
            if ($this->hasParam($f)) $updates[$f] = $this->getParam($f);
        }
        if ($this->hasParam('question_type')) {
            $type = $this->getParam('question_type');
            $allowed = ['yes_no_partial', 'single_choice', 'multi_choice', 'scale_1_5', 'text', 'number', 'file'];
            if (!in_array($type, $allowed, true)) $this->jsonError('Tipo domanda non valido', 422, 'INVALID_TYPE');
            $updates['question_type'] = $type;
        }
        if ($this->hasParam('options')) {
            $opt = $this->getParam('options');
            $updates['options'] = $opt !== null ? json_encode($opt, JSON_UNESCAPED_UNICODE) : null;
        }
        if ($this->hasParam('weight'))      $updates['weight'] = (float) $this->getParam('weight');
        if ($this->hasParam('order_index')) $updates['order_index'] = (int) $this->getParam('order_index');
        if ($this->hasParam('is_required')) $updates['is_required'] = $this->getParam('is_required') ? 1 : 0;
        if ($this->hasParam('high_criticality_only')) $updates['high_criticality_only'] = $this->getParam('high_criticality_only') ? 1 : 0;

        if (!empty($updates)) {
            Database::update('questionnaire_questions', $updates, 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
            $this->logAudit('questionnaire_question_updated', 'questionnaire_question', $id, $updates);
        }
        $this->jsonSuccess($updates, 'Domanda aggiornata');
    }

    /** DELETE /api/supply-chain/questions/{id} — elimina domanda. */
    public function deleteTemplateQuestion(int $id): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $deleted = Database::delete('questionnaire_questions', 'id = ? AND organization_id = ?', [$id, $this->getCurrentOrgId()]);
        if ($deleted === 0) {
            $this->jsonError('Domanda non trovata', 404, 'NOT_FOUND');
        }
        $this->logAudit('questionnaire_question_deleted', 'questionnaire_question', $id);
        $this->jsonSuccess(null, 'Domanda eliminata');
    }

    /** POST /api/supply-chain/import — import massivo fornitori (CSV/JSON), upsert per external_ref. */
    public function importSuppliers(): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $body = $this->getJsonBody();
        $rows = $body['suppliers'] ?? $body['rows'] ?? null;
        if (!is_array($rows) || empty($rows)) {
            $this->jsonError('Nessun fornitore da importare (campo "suppliers" array)', 422, 'NO_ROWS');
        }
        if (count($rows) > 1000) {
            $this->jsonError('Troppi record in un singolo import (max 1000)', 422, 'TOO_MANY');
        }

        $result = $this->bulkUpsertSuppliers($rows, 'csv');
        $this->logAudit('suppliers_imported', 'supplier', null, $result);
        $this->jsonSuccess($result, 'Import completato');
    }

    /**
     * Upsert massivo fornitori per (organization_id, external_ref). Riusato da import CSV
     * e da ServicesController (API key, scope write:suppliers). Sanitizza i campi.
     * @return array{created:int,updated:int,skipped:int,errors:array}
     */
    public function bulkUpsertSuppliers(array $rows, string $source): array
    {
        $orgId = $this->getCurrentOrgId();
        $created = 0; $updated = 0; $skipped = 0; $errors = [];

        foreach ($rows as $i => $r) {
            $name = trim((string) ($r['name'] ?? ''));
            if ($name === '') { $skipped++; $errors[] = ['row' => $i, 'error' => 'name mancante']; continue; }

            $extRef = isset($r['external_ref']) ? trim((string) $r['external_ref']) : null;
            $extRef = ($extRef === '') ? null : $extRef;

            $crit = strtolower((string) ($r['criticality'] ?? 'medium'));
            if (!in_array($crit, ['low', 'medium', 'high', 'critical'], true)) $crit = 'medium';

            $catId = null;
            if (!empty($r['category_slug'])) {
                $cat = Database::fetchOne(
                    'SELECT id FROM supplier_categories WHERE slug = ? AND organization_id IN (0, ?)',
                    [$this->slugify((string) $r['category_slug']), $orgId]
                );
                $catId = $cat['id'] ?? null;
            } elseif (!empty($r['category_id'])) {
                // Verifica scope: la categoria deve essere un preset (org 0) o della stessa org,
                // altrimenti si scriverebbe un category_id di un'altra org (dato sporco cross-org).
                $cat = Database::fetchOne(
                    'SELECT id FROM supplier_categories WHERE id = ? AND organization_id IN (0, ?)',
                    [(int) $r['category_id'], $orgId]
                );
                $catId = $cat['id'] ?? null;
            }

            $data = [
                'name'                => $name,
                'vat_number'          => $this->sanitizeCell($r['vat_number'] ?? null),
                'contact_email'       => $this->sanitizeCell($r['contact_email'] ?? null),
                'contact_name'        => $this->sanitizeCell($r['contact_name'] ?? null),
                'service_type'        => $this->sanitizeCell($r['service_type'] ?? 'altro'),
                'service_description' => $this->sanitizeCell($r['service_description'] ?? null),
                'criticality'         => $crit,
                'category_id'         => $catId,
            ];

            // Upsert per external_ref (se presente), altrimenti insert nuovo.
            $existing = null;
            if ($extRef !== null) {
                $existing = Database::fetchOne(
                    'SELECT id FROM suppliers WHERE organization_id = ? AND external_ref = ?',
                    [$orgId, $extRef]
                );
            }

            if ($existing) {
                Database::update('suppliers', $data, 'id = ? AND organization_id = ?', [$existing['id'], $orgId]);
                $updated++;
            } else {
                Database::insert('suppliers', array_merge($data, [
                    'organization_id' => $orgId,
                    'external_ref'    => $extRef,
                    'source'          => $source,
                ]));
                $created++;
            }
        }

        return ['created' => $created, 'updated' => $updated, 'skipped' => $skipped, 'errors' => array_slice($errors, 0, 50)];
    }

    // --- helper privati Fase 1 ---

    /** Verifica che la categoria sia visibile all'org (preset o propria). */
    private function assertCategoryVisible(int $categoryId): void
    {
        $cat = Database::fetchOne(
            'SELECT id FROM supplier_categories WHERE id = ? AND organization_id IN (0, ?)',
            [$categoryId, $this->getCurrentOrgId()]
        );
        if (!$cat) {
            $this->jsonError('Categoria non valida', 422, 'INVALID_CATEGORY');
        }
    }

    /** Verifica che il template appartenga all'org corrente. */
    private function assertTemplateOwned(int $templateId): void
    {
        $tpl = Database::fetchOne(
            'SELECT id FROM questionnaire_templates WHERE id = ? AND organization_id = ?',
            [$templateId, $this->getCurrentOrgId()]
        );
        if (!$tpl) {
            $this->jsonError('Template non trovato', 404, 'NOT_FOUND');
        }
    }

    /** Slug stabile: minuscole, alfanumerico + underscore. */
    private function slugify(string $s): string
    {
        $s = strtolower(trim($s));
        $s = preg_replace('/[^a-z0-9]+/', '_', $s);
        return trim((string) $s, '_');
    }

    /** Sanitizza una cella import: anti formula-injection CSV + trim. */
    private function sanitizeCell($v): ?string
    {
        if ($v === null) return null;
        $v = trim((string) $v);
        if ($v === '') return null;
        // neutralizza formula-injection (Excel/Sheets) prefissando un apice
        if (preg_match('/^[=+\-@]/', $v)) {
            $v = "'" . $v;
        }
        return $v;
    }

    // ══════════════════════════════════════════════════════════════════════
    // CAMPAGNE QUESTIONARIO (Fase 2) — questionnaire_campaigns + scadenze/cruscotto
    // ══════════════════════════════════════════════════════════════════════

    /**
     * POST /api/supply-chain/{supplierId}/campaigns
     * Crea una campagna (invia un template a un fornitore) con scadenza/ricorrenza/reminder.
     * Genera token sq_ (retrocompat portale), crea il supplier_user per l'accesso OTP,
     * invia l'email di invito col link al portale.
     */
    public function createCampaign(int $supplierId): void
    {
        $this->requireOrgRole(['org_admin', 'compliance_manager']);
        $orgId = $this->getCurrentOrgId();

        $supplier = Database::fetchOne(
            'SELECT id, name, contact_email, criticality FROM suppliers WHERE id = ? AND organization_id = ? AND deleted_at IS NULL',
            [$supplierId, $orgId]
        );
        if (!$supplier) {
            $this->jsonError('Fornitore non trovato', 404, 'NOT_FOUND');
        }

        $templateId = (int) $this->getParam('template_id');
        $tpl = Database::fetchOne(
            'SELECT id, current_version FROM questionnaire_templates WHERE id = ? AND organization_id = ?',
            [$templateId, $orgId]
        );
        if (!$tpl) {
            $this->jsonError('Template non valido', 422, 'INVALID_TEMPLATE');
        }

        $email = trim((string) ($this->getParam('email') ?: $supplier['contact_email']));
        if ($email === '') {
            $this->jsonError('Email del referente fornitore mancante', 422, 'EMAIL_REQUIRED');
        }

        // Scadenza configurabile (nessun default imposto). Reminder e ricorrenza opzionali.
        $dueDays = $this->getParam('due_days');
        $dueAt   = ($dueDays !== null && $dueDays !== '') ? date('Y-m-d H:i:s', time() + ((int) $dueDays) * 86400) : null;
        $recurrenceMonths = $this->getParam('recurrence_months');
        $recurrenceMonths = ($recurrenceMonths !== null && $recurrenceMonths !== '') ? (int) $recurrenceMonths : null;
        $isRecurring = $recurrenceMonths ? 1 : 0;
        $nextRecurrenceAt = ($isRecurring && $dueAt) ? date('Y-m-d H:i:s', strtotime($dueAt . " +{$recurrenceMonths} months")) : null;

        // reminder_offsets: giorni PRIMA della scadenza (default [-7,-1] se c'è due_at)
        $offsets = $this->getParam('reminder_offsets');
        if (!is_array($offsets)) $offsets = $dueAt ? [-7, -1] : [];
        $offsets = array_values(array_filter(array_map('intval', $offsets), fn($o) => $o < 0));
        $nextReminderAt = $this->computeNextReminder($dueAt, $offsets, null);

        $rawToken = 'sq_' . bin2hex(random_bytes(20));
        $expires  = $dueAt ?: date('Y-m-d H:i:s', time() + 30 * 86400);
        $showScore = $this->getParam('show_score_to_supplier') ? 1 : 0;
        $language  = in_array($this->getParam('language'), ['it', 'en', 'auto'], true) ? $this->getParam('language') : 'it';

        $campaignId = Database::insert('questionnaire_campaigns', [
            'organization_id'        => $orgId,
            'supplier_id'            => $supplierId,
            'template_id'            => $templateId,
            'template_version'       => $tpl['current_version'],
            'access_token_hash'      => hash('sha256', $rawToken),
            'status'                 => 'sent',
            'show_score_to_supplier' => $showScore,
            'language'               => $language,
            'sent_to_email'          => $email,
            'sent_at'                => date('Y-m-d H:i:s'),
            'due_at'                 => $dueAt,
            'expires_at'             => $expires,
            'reminder_offsets'       => json_encode($offsets),
            'next_reminder_at'       => $nextReminderAt,
            'is_recurring'           => $isRecurring,
            'recurrence_months'      => $recurrenceMonths,
            'next_recurrence_at'     => $nextRecurrenceAt,
            'created_by'             => $this->getCurrentUserId(),
        ]);

        // Crea/aggiorna il supplier_user per l'accesso al portale (OTP) — idempotente.
        $existingUser = Database::fetchOne(
            'SELECT id FROM supplier_users WHERE supplier_id = ? AND email = ?',
            [$supplierId, $email]
        );
        if (!$existingUser) {
            Database::insert('supplier_users', [
                'supplier_id'     => $supplierId,
                'organization_id' => $orgId,
                'email'           => $email,
                'role'            => 'contact',
                'is_active'       => 1,
            ]);
        }

        $portalLink = 'https://nis2.agile.software/supplier-portal.html';
        $legacyLink = 'https://nis2.agile.software/supplier-assessment.html?token=' . $rawToken;
        $dueLabel = $dueAt ? (' entro il ' . date('d/m/Y', strtotime($dueAt))) : '';

        try {
            if (class_exists('EmailService')) {
                $svc = new EmailService();
                $svc->send($email, 'Questionario di sicurezza fornitore - NIS2',
                    "<p>Gentile referente di <strong>" . htmlspecialchars($supplier['name']) . "</strong>,</p>"
                    . "<p>vi chiediamo di compilare un questionario di sicurezza richiesto dalla normativa NIS2{$dueLabel}.</p>"
                    . "<p>Accedete al <strong>portale fornitori</strong> con la vostra email: <a href=\"{$portalLink}\">{$portalLink}</a></p>"
                    . "<p style=\"font-size:13px;color:#666\">In alternativa potete usare il link diretto: <a href=\"{$legacyLink}\">compila qui</a></p>");
            }
        } catch (Throwable $e) {
            error_log('[CAMPAIGN] email: ' . $e->getMessage());
        }

        $this->logAudit('questionnaire_campaign_created', 'questionnaire_campaign', $campaignId, ['supplier_id' => $supplierId, 'template_id' => $templateId]);
        $this->jsonSuccess([
            'campaign_id' => $campaignId,
            'sent_to'     => $email,
            'due_at'      => $dueAt,
            'portal_link' => $portalLink,
            'link'        => $legacyLink,
        ], 'Campagna creata e invito inviato', 201);
    }

    /**
     * GET /api/supply-chain/campaigns — cruscotto campagne con semaforo scadenze.
     */
    public function campaigns(): void
    {
        $this->requireOrgAccess();
        $rows = Database::fetchAll(
            'SELECT c.id, c.supplier_id, s.name AS supplier_name, c.template_id, t.name AS template_name,
                    c.status, c.score, c.risk_level, c.sent_to_email, c.sent_at, c.due_at, c.completed_at,
                    c.reminder_count, c.is_recurring, c.recurrence_months,
                    (SELECT COUNT(*) FROM questionnaire_answers a WHERE a.campaign_id = c.id) AS answers_count
               FROM questionnaire_campaigns c
               LEFT JOIN suppliers s ON s.id = c.supplier_id
               LEFT JOIN questionnaire_templates t ON t.id = c.template_id
              WHERE c.organization_id = ?
              ORDER BY (c.status IN ("sent","in_progress")) DESC, c.due_at IS NULL, c.due_at ASC, c.sent_at DESC',
            [$this->getCurrentOrgId()]
        );
        // Semaforo calcolato lato server: in_regola / in_scadenza (<=7gg) / scaduto
        $now = time();
        foreach ($rows as &$r) {
            $light = 'neutral';
            if ($r['status'] === 'completed') {
                $light = 'success';
            } elseif ($r['status'] === 'expired') {
                $light = 'danger';
            } elseif (!empty($r['due_at'])) {
                $days = (strtotime($r['due_at']) - $now) / 86400;
                $light = $days < 0 ? 'danger' : ($days <= 7 ? 'warning' : 'neutral');
            }
            $r['semaphore'] = $light;
        }
        unset($r);
        $this->jsonSuccess(['campaigns' => $rows]);
    }

    /** Calcola il prossimo next_reminder_at dato due_at, offset (giorni negativi) e l'ultimo già inviato. */
    private function computeNextReminder(?string $dueAt, array $offsets, ?string $afterTs): ?string
    {
        if (!$dueAt || empty($offsets)) return null;
        $due = strtotime($dueAt);
        $after = $afterTs ? strtotime($afterTs) : time();
        $candidates = [];
        foreach ($offsets as $o) {
            $ts = $due + ((int) $o) * 86400; // o è negativo → prima della scadenza
            if ($ts > $after) $candidates[] = $ts;
        }
        if (empty($candidates)) return null;
        return date('Y-m-d H:i:s', min($candidates));
    }

    /** Risolve un token grezzo nel record questionario, validando scadenza. */
    private function resolveQuestionnaire(string $rawToken): array
    {
        if (!preg_match('/^sq_[a-f0-9]{40}$/', $rawToken)) {
            $this->jsonError('Token non valido', 404, 'INVALID_TOKEN');
        }
        $q = Database::fetchOne('SELECT * FROM supplier_questionnaires WHERE token_hash = ?', [hash('sha256', $rawToken)]);
        if (!$q) {
            $this->jsonError('Questionario non trovato', 404, 'NOT_FOUND');
        }
        if ($q['expires_at'] && strtotime($q['expires_at']) < time() && $q['status'] !== 'completed') {
            Database::query('UPDATE supplier_questionnaires SET status=? WHERE id=?', ['expired', $q['id']]);
            $this->jsonError('Questionario scaduto', 410, 'EXPIRED');
        }
        return $q;
    }
}
