<?php
/**
 * NIS2 Agile - Base Controller
 *
 * Classe base per tutti i controller.
 * Gestisce autenticazione, multi-tenancy, risposte JSON, validazione.
 */

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

class BaseController
{
    protected ?array $currentUser = null;
    protected ?array $currentSession = null;
    protected ?int $currentOrgId = null;
    protected ?string $currentOrgRole = null;

    // ═══════════════════════════════════════════════════════════════════════
    // RISPOSTE JSON
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Invia risposta JSON di successo
     */
    protected function jsonSuccess($data = null, string $message = 'OK', int $statusCode = 200): void
    {
        http_response_code($statusCode);
        header('Content-Type: application/json; charset=utf-8');

        echo json_encode([
            'success' => true,
            'message' => $message,
            'data'    => $data,
        ], JSON_UNESCAPED_UNICODE);

        exit;
    }

    /**
     * Invia risposta JSON di errore
     */
    protected function jsonError(string $message, int $statusCode = 400, ?string $errorCode = null, ?array $data = null): void
    {
        http_response_code($statusCode);
        header('Content-Type: application/json; charset=utf-8');

        $response = [
            'success' => false,
            'message' => $message,
        ];

        if ($errorCode) {
            $response['error_code'] = $errorCode;
        }

        if ($data) {
            $response['data'] = $data;
        }

        echo json_encode($response, JSON_UNESCAPED_UNICODE);
        exit;
    }

    /**
     * Invia risposta paginata
     */
    protected function jsonPaginated(array $items, int $total, int $page, int $perPage): void
    {
        $this->jsonSuccess([
            'items'     => $items,
            'total'     => $total,
            'page'      => $page,
            'per_page'  => $perPage,
            'pages'     => ceil($total / $perPage),
        ]);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // PARAMETRI RICHIESTA
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Ottiene parametro dalla richiesta (GET, POST o JSON body)
     */
    protected function getParam(string $key, $default = null)
    {
        if (isset($_REQUEST[$key])) {
            return $_REQUEST[$key];
        }

        $jsonBody = $this->getJsonBody();
        if (isset($jsonBody[$key])) {
            return $jsonBody[$key];
        }

        return $default;
    }

    /**
     * Verifica se un parametro esiste
     */
    protected function hasParam(string $key): bool
    {
        if (isset($_REQUEST[$key])) {
            return true;
        }

        $jsonBody = $this->getJsonBody();
        return isset($jsonBody[$key]);
    }

    /**
     * Ottiene tutti i parametri dalla richiesta
     */
    protected function getAllParams(): array
    {
        $params = $_REQUEST;
        $jsonBody = $this->getJsonBody();
        return array_merge($params, $jsonBody);
    }

    /**
     * Ottiene il body JSON della richiesta
     */
    protected function getJsonBody(): array
    {
        static $jsonBody = null;

        if ($jsonBody === null) {
            $input = file_get_contents('php://input');
            $jsonBody = json_decode($input, true) ?? [];
        }

        return $jsonBody;
    }

    /**
     * Ottiene parametri di paginazione
     */
    protected function getPagination(int $defaultPerPage = 20): array
    {
        $page = max(1, (int) $this->getParam('page', 1));
        $perPage = min(100, max(1, (int) $this->getParam('per_page', $defaultPerPage)));
        $offset = ($page - 1) * $perPage;

        return ['page' => $page, 'per_page' => $perPage, 'offset' => $offset];
    }

    // ═══════════════════════════════════════════════════════════════════════
    // VALIDAZIONE
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Valida parametri obbligatori
     */
    protected function validateRequired(array $required): void
    {
        $missing = [];

        foreach ($required as $field) {
            $value = $this->getParam($field);
            if ($value === null || $value === '') {
                $missing[] = $field;
            }
        }

        if (!empty($missing)) {
            $this->jsonError(
                'Campi obbligatori mancanti: ' . implode(', ', $missing),
                400,
                'MISSING_REQUIRED_FIELDS'
            );
        }
    }

    /**
     * Valida formato email
     */
    protected function validateEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    /**
     * Valida Partita IVA italiana
     */
    protected function validateVAT(string $vat): bool
    {
        $vat = preg_replace('/\s+/', '', $vat);
        $vat = preg_replace('/^IT/i', '', $vat);

        if (!preg_match('/^\d{11}$/', $vat)) {
            return false;
        }

        $sum = 0;
        for ($i = 0; $i < 11; $i++) {
            $digit = (int) $vat[$i];
            if ($i % 2 === 0) {
                $sum += $digit;
            } else {
                $double = $digit * 2;
                $sum += ($double > 9) ? $double - 9 : $double;
            }
        }

        return ($sum % 10) === 0;
    }

    /**
     * Valida Codice Fiscale italiano
     */
    protected function validateFiscalCode(string $cf): bool
    {
        $cf = strtoupper(trim($cf));
        return (bool) preg_match('/^[A-Z0-9]{16}$/', $cf);
    }

    /**
     * Valida password secondo policy
     */
    protected function validatePassword(string $password): array
    {
        $errors = [];

        if (strlen($password) < PASSWORD_MIN_LENGTH) {
            $errors[] = 'La password deve essere di almeno ' . PASSWORD_MIN_LENGTH . ' caratteri';
        }

        if (PASSWORD_REQUIRE_UPPERCASE && !preg_match('/[A-Z]/', $password)) {
            $errors[] = 'La password deve contenere almeno una lettera maiuscola';
        }

        if (PASSWORD_REQUIRE_NUMBER && !preg_match('/[0-9]/', $password)) {
            $errors[] = 'La password deve contenere almeno un numero';
        }

        if (PASSWORD_REQUIRE_SPECIAL && !preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
            $errors[] = 'La password deve contenere almeno un carattere speciale';
        }

        return $errors;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // AUTENTICAZIONE JWT
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Richiede autenticazione JWT
     */
    protected function requireAuth(): void
    {
        $token = $this->getBearerToken();

        if (!$token) {
            $this->jsonError('Token di autenticazione mancante', 401, 'MISSING_TOKEN');
        }

        $payload = $this->verifyJWT($token);

        if (!$payload) {
            $this->jsonError('Token non valido o scaduto', 401, 'INVALID_TOKEN');
        }

        $user = Database::fetchOne(
            'SELECT * FROM users WHERE id = ? AND is_active = 1',
            [$payload['user_id']]
        );

        if (!$user) {
            $this->jsonError('Utente non trovato o disabilitato', 401, 'USER_NOT_FOUND');
        }

        // --- Multi-device session verification (Fase 2 / G06) ---
        // JWT senza jti = legacy (pre Fase 2) → rifiutato per forzare nuovo login con sessione tracciata.
        $jti = $payload['jti'] ?? null;
        if (!$jti) {
            $this->jsonError('Token senza session id — effettua nuovamente il login', 401, 'JWT_NO_JTI');
        }
        $session = Database::fetchOne(
            'SELECT * FROM active_sessions
             WHERE id = ? AND user_id = ? AND revoked_at IS NULL AND expires_at > NOW()',
            [$jti, (int) $user['id']]
        );
        if (!$session) {
            $this->jsonError('Sessione non valida o revocata', 401, 'SESSION_REVOKED');
        }
        // Throttle last_activity update: max 1 update/min per evitare write storm
        $lastTs = strtotime($session['last_activity_at']);
        if ($lastTs && (time() - $lastTs) > 60) {
            Database::update('active_sessions',
                ['last_activity_at' => date('Y-m-d H:i:s')],
                'id = ?',
                [$jti]
            );
        }
        $user['session_jti'] = $jti;
        $this->currentSession = $session;

        $this->currentUser = $user;
    }

    /**
     * Parse User-Agent in label friendly: "Chrome 134 su Windows", "Safari su macOS", ecc.
     * Fallback "Browser sconosciuto" se UA assente/malformato.
     */
    protected function parseDeviceLabel(string $ua): string
    {
        if ($ua === '') return 'Browser sconosciuto';
        $os = 'OS sconosciuto';
        if (preg_match('/Windows NT 10/i', $ua))             $os = 'Windows';
        elseif (preg_match('/Mac OS X|Macintosh/i', $ua))    $os = 'macOS';
        elseif (preg_match('/Android/i', $ua))               $os = 'Android';
        elseif (preg_match('/iPhone|iPad|iOS/i', $ua))       $os = 'iOS';
        elseif (preg_match('/Linux/i', $ua))                 $os = 'Linux';

        $browser = 'Browser';
        if (preg_match('/Edg\/([0-9]+)/', $ua, $m))          $browser = 'Edge ' . $m[1];
        elseif (preg_match('/OPR\/([0-9]+)/', $ua, $m))      $browser = 'Opera ' . $m[1];
        elseif (preg_match('/Chrome\/([0-9]+)/', $ua, $m))   $browser = 'Chrome ' . $m[1];
        elseif (preg_match('/Firefox\/([0-9]+)/', $ua, $m))  $browser = 'Firefox ' . $m[1];
        elseif (preg_match('/Safari\/([0-9]+)/', $ua, $m) && !preg_match('/Chrome/', $ua)) {
            if (preg_match('/Version\/([0-9]+)/', $ua, $m2)) $browser = 'Safari ' . $m2[1];
            else $browser = 'Safari';
        }
        return substr($browser . ' su ' . $os, 0, 120);
    }

    /**
     * Richiede ruolo super_admin
     */
    protected function requireSuperAdmin(): void
    {
        $this->requireAuth();

        if ($this->currentUser['role'] !== 'super_admin') {
            $this->jsonError('Accesso riservato ai super amministratori', 403, 'SUPER_ADMIN_REQUIRED');
        }
    }

    // ═══════════════════════════════════════════════════════════════════════
    // MULTI-TENANCY
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Richiede accesso all'organizzazione corrente
     */
    protected function requireOrgAccess(): void
    {
        $this->requireAuth();

        $orgId = $this->resolveOrgId();

        if (!$orgId) {
            $this->jsonError('Organizzazione non selezionata', 403, 'NO_ORG');
        }

        // Super admin ha accesso a tutto
        if ($this->currentUser['role'] === 'super_admin') {
            $this->currentOrgId = $orgId;
            $this->currentOrgRole = 'super_admin';
            return;
        }

        // Verifica membership
        $membership = Database::fetchOne(
            'SELECT role FROM user_organizations WHERE user_id = ? AND organization_id = ?',
            [$this->getCurrentUserId(), $orgId]
        );

        if (!$membership) {
            $this->jsonError('Accesso non autorizzato a questa organizzazione', 403, 'ORG_ACCESS_DENIED');
        }

        $this->currentOrgId = $orgId;
        $this->currentOrgRole = $membership['role'];
    }

    /**
     * Richiede ruolo minimo nell'organizzazione
     */
    protected function requireOrgRole(array $allowedRoles): void
    {
        $this->requireOrgAccess();

        if ($this->currentOrgRole === 'super_admin') {
            return;
        }

        if (!in_array($this->currentOrgRole, $allowedRoles)) {
            $this->jsonError(
                'Ruolo insufficiente. Richiesto: ' . implode(' o ', $allowedRoles),
                403,
                'INSUFFICIENT_ROLE'
            );
        }
    }

    /**
     * Risolve l'ID organizzazione dalla richiesta
     */
    protected function resolveOrgId(): ?int
    {
        // 1. Header X-Organization-Id
        $orgId = $_SERVER['HTTP_X_ORGANIZATION_ID'] ?? null;
        if ($orgId) {
            return (int) $orgId;
        }

        // 2. Query parameter org_id
        $orgId = $this->getParam('org_id');
        if ($orgId) {
            return (int) $orgId;
        }

        // 3. Organizzazione primaria dell'utente
        $primary = Database::fetchOne(
            'SELECT organization_id FROM user_organizations WHERE user_id = ? AND is_primary = 1',
            [$this->getCurrentUserId()]
        );

        return $primary ? (int) $primary['organization_id'] : null;
    }

    /**
     * Ottiene utente corrente
     */
    protected function getCurrentUser(): ?array
    {
        return $this->currentUser;
    }

    /**
     * Ottiene ID utente corrente
     */
    protected function getCurrentUserId(): ?int
    {
        return $this->currentUser ? (int) $this->currentUser['id'] : null;
    }

    /**
     * Ottiene ID organizzazione corrente
     */
    protected function getCurrentOrgId(): ?int
    {
        return $this->currentOrgId;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // JWT TOKEN MANAGEMENT
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Estrae Bearer token dall'header Authorization
     */
    protected function getBearerToken(): ?string
    {
        $headers = $this->getAuthorizationHeader();

        if ($headers && preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
            return $matches[1];
        }

        if (isset($_GET['token']) && !empty($_GET['token'])) {
            return $_GET['token'];
        }

        return null;
    }

    /**
     * Ottiene header Authorization
     */
    private function getAuthorizationHeader(): ?string
    {
        if (isset($_SERVER['Authorization'])) {
            return $_SERVER['Authorization'];
        }

        if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
            return $_SERVER['HTTP_AUTHORIZATION'];
        }

        if (function_exists('apache_request_headers')) {
            $headers = apache_request_headers();
            if (isset($headers['Authorization'])) {
                return $headers['Authorization'];
            }
        }

        return null;
    }

    /**
     * Genera JWT token
     */
    protected function generateJWT(int $userId, array $extraData = []): string
    {
        $header = json_encode([
            'typ' => 'JWT',
            'alg' => JWT_ALGORITHM,
        ]);

        $payload = json_encode(array_merge([
            'user_id' => $userId,
            'iat'     => time(),
            'exp'     => time() + JWT_EXPIRES_IN,
        ], $extraData));

        $base64Header = $this->base64UrlEncode($header);
        $base64Payload = $this->base64UrlEncode($payload);

        $signature = hash_hmac('sha256', "$base64Header.$base64Payload", JWT_SECRET, true);
        $base64Signature = $this->base64UrlEncode($signature);

        return "$base64Header.$base64Payload.$base64Signature";
    }

    /**
     * Verifica JWT token
     */
    protected function verifyJWT(string $token): ?array
    {
        $parts = explode('.', $token);

        if (count($parts) !== 3) {
            return null;
        }

        [$base64Header, $base64Payload, $base64Signature] = $parts;

        $signature = $this->base64UrlDecode($base64Signature);
        $expectedSignature = hash_hmac('sha256', "$base64Header.$base64Payload", JWT_SECRET, true);

        if (!hash_equals($signature, $expectedSignature)) {
            return null;
        }

        $payload = json_decode($this->base64UrlDecode($base64Payload), true);

        if (!$payload) {
            return null;
        }

        if (isset($payload['exp']) && $payload['exp'] < time()) {
            return null;
        }

        return $payload;
    }

    /**
     * Genera refresh token.
     * Da Fase 2 (G05) supporta linking esplicito alla `active_sessions.id` via $sessionJti
     * per permettere rotazione safe e revoca cascade.
     */
    protected function generateRefreshToken(int $userId, ?string $sessionJti = null): string
    {
        $token = bin2hex(random_bytes(32));
        $expiresAt = date('Y-m-d H:i:s', time() + JWT_REFRESH_EXPIRES_IN);

        Database::insert('refresh_tokens', [
            'user_id'     => $userId,
            'token'       => hash('sha256', $token),
            'expires_at'  => $expiresAt,
            'session_jti' => $sessionJti,
        ]);

        return $token;
    }

    protected function base64UrlEncode(string $data): string
    {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }

    protected function base64UrlDecode(string $data): string
    {
        return base64_decode(strtr($data, '-_', '+/'));
    }

    // ═══════════════════════════════════════════════════════════════════════
    // AUDIT LOGGING
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Registra azione nell'audit log
     */
    protected function logAudit(string $action, ?string $entityType = null, ?int $entityId = null, ?array $details = null): void
    {
        $orgId    = $this->currentOrgId;
        $userId   = $this->getCurrentUserId();
        $severity = AuditService::resolveSeverity($action, $details);
        $email    = $this->currentUser['email'] ?? null;

        AuditService::log(
            $orgId ?? 0,
            $userId,
            $action,
            $entityType,
            $entityId,
            $details,
            $_SERVER['REMOTE_ADDR'] ?? '',
            $_SERVER['HTTP_USER_AGENT'] ?? null,
            $severity,
            $email
        );
    }

    // ═══════════════════════════════════════════════════════════════════════
    // UTILITY
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Sanitizza stringa per output
     */
    protected function sanitize(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
    }

    /**
     * Ottiene metodo HTTP della richiesta
     */
    protected function getMethod(): string
    {
        return strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
    }

    /**
     * Genera codice univoco
     */
    protected function generateCode(string $prefix, int $length = 6): string
    {
        $number = str_pad(mt_rand(0, pow(10, $length) - 1), $length, '0', STR_PAD_LEFT);
        return $prefix . '-' . $number;
    }
}
