<?php
/**
 * NIS2 Agile - Webhook Service
 *
 * Gestisce la consegna webhook outbound a sistemi esterni.
 * Pattern: Stripe-like HMAC-SHA256, retry 3x con backoff esponenziale.
 *
 * Firma header: X-NIS2-Signature: sha256=HMAC_SHA256(body, secret)
 * Retry: 1° tentativo immediato → 2° dopo 5min → 3° dopo 30min
 */

require_once APP_PATH . '/config/database.php';

class WebhookService
{
    // Retry schedule: secondi di attesa per tentativo
    private const RETRY_DELAYS = [0, 300, 1800]; // 0s, 5min, 30min
    private const TIMEOUT_SEC  = 10;              // Timeout HTTP per delivery
    private const MAX_RESPONSE_LEN = 2048;        // Max byte di risposta salvata

    // ══════════════════════════════════════════════════════════════════════
    // DISPATCH (entry point principale)
    // ══════════════════════════════════════════════════════════════════════

    /**
     * Dispatcha un evento a tutte le subscription attive per quell'org/evento.
     * Si chiama sincrono nel request cycle (fire-and-forget con best effort).
     *
     * @param int    $orgId     Organizzazione proprietaria
     * @param string $eventType Es. "incident.created"
     * @param array  $payload   Dati dell'evento
     * @param string $eventId   UUID univoco evento (idempotency)
     */
    public function dispatch(int $orgId, string $eventType, array $payload, string $eventId = ''): void
    {
        if (empty($eventId)) {
            $eventId = $this->generateUuid();
        }

        // Trova subscription attive che ascoltano questo evento
        $subscriptions = Database::fetchAll(
            'SELECT * FROM webhook_subscriptions
             WHERE organization_id = ? AND is_active = 1 AND failure_count < 10
             ORDER BY id',
            [$orgId]
        );

        foreach ($subscriptions as $sub) {
            $events = json_decode($sub['events'], true) ?? [];
            if (!in_array($eventType, $events) && !in_array('*', $events)) {
                continue;
            }

            // Crea delivery record
            $fullPayload = $this->buildPayload($eventType, $eventId, $payload, $sub);
            $deliveryId  = $this->createDelivery($sub, $eventType, $eventId, $fullPayload);

            // Tenta consegna immediata
            $this->attemptDelivery($deliveryId, $sub, $fullPayload);
        }
    }

    /**
     * Processa retry pendenti (chiamato via cron o endpoint admin).
     */
    public function processRetries(): int
    {
        $pending = Database::fetchAll(
            'SELECT wd.*, ws.url, ws.secret
             FROM webhook_deliveries wd
             JOIN webhook_subscriptions ws ON ws.id = wd.subscription_id
             WHERE wd.status = "retrying"
               AND wd.next_retry_at <= NOW()
               AND wd.attempt <= 3
             LIMIT 50'
        );

        $processed = 0;
        foreach ($pending as $delivery) {
            $sub = ['id' => $delivery['subscription_id'], 'url' => $delivery['url'], 'secret' => $delivery['secret']];
            $this->attemptDelivery($delivery['id'], $sub, json_decode($delivery['payload'], true));
            $processed++;
        }

        return $processed;
    }

    // ══════════════════════════════════════════════════════════════════════
    // INTERNAL
    // ══════════════════════════════════════════════════════════════════════

    /**
     * Costruisce il payload completo con envelope standard NIS2.
     */
    private function buildPayload(string $eventType, string $eventId, array $data, array $sub): array
    {
        return [
            'id'          => $eventId,
            'event'       => $eventType,
            'api_version' => '1.0.0',
            'created'     => time(),
            'created_at'  => date('c'),
            'source'      => 'nis2-agile',
            'org_id'      => $sub['organization_id'],
            'data'        => $data,
        ];
    }

    /**
     * Crea record delivery nel DB, restituisce ID.
     */
    private function createDelivery(array $sub, string $eventType, string $eventId, array $payload): int
    {
        return Database::insert('webhook_deliveries', [
            'subscription_id' => $sub['id'],
            'organization_id' => $sub['organization_id'],
            'event_type'      => $eventType,
            'event_id'        => $eventId,
            'payload'         => json_encode($payload, JSON_UNESCAPED_UNICODE),
            'status'          => 'pending',
            'attempt'         => 1,
        ]);
    }

    /**
     * Tenta la consegna HTTP di un webhook.
     * Aggiorna il record delivery con il risultato.
     */
    private function attemptDelivery(int $deliveryId, array $sub, array $payload): void
    {
        $bodyJson  = json_encode($payload, JSON_UNESCAPED_UNICODE);
        $signature = 'sha256=' . hash_hmac('sha256', $bodyJson, $sub['secret']);
        $attempt   = $payload['_attempt'] ?? 1;

        $httpCode     = null;
        $responseBody = null;
        $success      = false;

        try {
            $ch = curl_init($sub['url']);
            curl_setopt_array($ch, [
                CURLOPT_POST            => true,
                CURLOPT_POSTFIELDS      => $bodyJson,
                CURLOPT_RETURNTRANSFER  => true,
                CURLOPT_TIMEOUT         => self::TIMEOUT_SEC,
                CURLOPT_CONNECTTIMEOUT  => 5,
                CURLOPT_FOLLOWLOCATION  => false,
                CURLOPT_SSL_VERIFYPEER  => true,
                CURLOPT_HTTPHEADER      => [
                    'Content-Type: application/json',
                    'User-Agent: NIS2-Agile-Webhooks/1.0',
                    'X-NIS2-Signature: ' . $signature,
                    'X-NIS2-Event: ' . $payload['event'],
                    'X-NIS2-Delivery-Id: ' . $deliveryId,
                    'X-NIS2-Attempt: ' . $attempt,
                ],
            ]);

            $responseBody = curl_exec($ch);
            $httpCode     = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            // Successo: qualsiasi 2xx
            $success = ($httpCode >= 200 && $httpCode < 300);

        } catch (Throwable $e) {
            $responseBody = 'Exception: ' . $e->getMessage();
            $httpCode     = 0;
        }

        // Tronca response
        if (strlen($responseBody) > self::MAX_RESPONSE_LEN) {
            $responseBody = substr($responseBody, 0, self::MAX_RESPONSE_LEN) . '...[truncated]';
        }

        if ($success) {
            // Delivery riuscita
            Database::execute(
                'UPDATE webhook_deliveries
                 SET status = "delivered", http_status = ?, response_body = ?,
                     delivered_at = NOW(), updated_at = NOW()
                 WHERE id = ?',
                [$httpCode, $responseBody, $deliveryId]
            );

            // Reset failure count
            Database::execute(
                'UPDATE webhook_subscriptions SET failure_count = 0, last_triggered_at = NOW() WHERE id = ?',
                [$sub['id']]
            );

        } else {
            // Calcola prossimo retry
            $nextAttempt = $attempt + 1;
            if ($nextAttempt <= 3) {
                $delay = self::RETRY_DELAYS[$attempt] ?? 1800;
                $nextRetry = date('Y-m-d H:i:s', time() + $delay);
                Database::execute(
                    'UPDATE webhook_deliveries
                     SET status = "retrying", http_status = ?, response_body = ?,
                         attempt = ?, next_retry_at = ?, updated_at = NOW()
                     WHERE id = ?',
                    [$httpCode, $responseBody, $nextAttempt, $nextRetry, $deliveryId]
                );
            } else {
                // Tutti i tentativi esauriti
                Database::execute(
                    'UPDATE webhook_deliveries
                     SET status = "failed", http_status = ?, response_body = ?,
                         updated_at = NOW()
                     WHERE id = ?',
                    [$httpCode, $responseBody, $deliveryId]
                );

                // Incrementa failure count subscription
                Database::execute(
                    'UPDATE webhook_subscriptions
                     SET failure_count = failure_count + 1, updated_at = NOW()
                     WHERE id = ?',
                    [$sub['id']]
                );
            }
        }
    }

    // ── Payload builders per evento ───────────────────────────────────────

    /**
     * Costruisce payload per evento incident.created / incident.updated
     */
    public static function incidentPayload(array $incident, string $action = 'created'): array
    {
        return [
            'action'    => $action,
            'incident'  => [
                'id'              => $incident['id'],
                'title'           => $incident['title'],
                'classification'  => $incident['classification'],
                'severity'        => $incident['severity'],
                'status'          => $incident['status'] ?? 'detected',
                'is_significant'  => (bool)$incident['is_significant'],
                'detected_at'     => $incident['detected_at'],
                'art23_deadlines' => [
                    'early_warning' => date('c', strtotime($incident['detected_at']) + 86400),
                    'notification'  => date('c', strtotime($incident['detected_at']) + 259200),
                    'final_report'  => date('c', strtotime($incident['detected_at']) + 2592000),
                ],
            ],
        ];
    }

    /**
     * Costruisce payload per evento risk.high_created / risk.critical_created
     */
    public static function riskPayload(array $risk, string $action = 'created'): array
    {
        return [
            'action' => $action,
            'risk'   => [
                'id'          => $risk['id'],
                'title'       => $risk['title'],
                'category'    => $risk['category'],
                'likelihood'  => $risk['likelihood'],
                'impact'      => $risk['impact'],
                'risk_level'  => $risk['risk_level'],
                'risk_score'  => $risk['inherent_risk_score'],
                'status'      => $risk['status'] ?? 'identified',
                'created_at'  => $risk['created_at'],
            ],
        ];
    }

    /**
     * Costruisce payload per evento policy.approved
     */
    public static function policyPayload(array $policy): array
    {
        return [
            'action' => 'approved',
            'policy' => [
                'id'          => $policy['id'],
                'title'       => $policy['title'],
                'category'    => $policy['category'],
                'nis2_article'=> $policy['nis2_article'],
                'version'     => $policy['version'],
                'approved_at' => $policy['approved_at'],
                'ai_generated'=> (bool)$policy['ai_generated'],
            ],
        ];
    }

    /**
     * Costruisce payload per evento compliance.score_changed
     */
    public static function scorePayload(int $orgId, ?int $previousScore, int $newScore): array
    {
        return [
            'previous_score' => $previousScore,
            'new_score'      => $newScore,
            'delta'          => $newScore - ($previousScore ?? 0),
            'label'          => $newScore >= 80 ? 'compliant'
                              : ($newScore >= 60 ? 'substantially_compliant'
                              : ($newScore >= 40 ? 'partial' : 'significant_gaps')),
        ];
    }

    // ── UUID ──────────────────────────────────────────────────────────────

    private function generateUuid(): string
    {
        return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }
}
