<?php
namespace App\Services;

use App\Core\Database;
use PDO;
use RuntimeException;
use Throwable;

/**
 * Sends native phone notifications without changing the existing in-app/email channels.
 *
 * Android tokens from @capacitor/push-notifications are Firebase registration tokens.
 * iOS tokens from @capacitor/push-notifications are APNs device tokens, so iOS delivery
 * is intentionally sent directly through APNs rather than incorrectly passing APNs tokens
 * to Firebase.
 */
final class NativePushService
{
    private array $config;
    private static ?array $cachedFirebaseAccessToken = null;
    private static ?array $cachedApnsJwt = null;

    public function __construct()
    {
        $this->config = $GLOBALS['config']['mobile_push'] ?? [];
    }

    public function configurationStatus(): array
    {
        $android = $this->androidConfigurationStatus();
        $ios = $this->iosConfigurationStatus();
        $configured = !empty($android['configured']) || !empty($ios['configured']);

        if (empty($this->config['enabled'])) {
            $message = 'Native phone push is disabled in config/app.php.';
        } elseif ($configured) {
            $ready = [];
            if (!empty($android['configured'])) $ready[] = 'Android';
            if (!empty($ios['configured'])) $ready[] = 'iOS';
            $message = implode(' and ', $ready) . ' phone push is configured.';
        } else {
            $message = 'Android: ' . $android['message'] . ' iOS: ' . $ios['message'];
        }

        return [
            'configured' => $configured,
            'message' => $message,
            'android' => $android,
            'ios' => $ios,
        ];
    }

    public function activeUserCount(array $userIds): int
    {
        $userIds = $this->cleanIds($userIds);
        if (!$userIds || !$this->tableExists('mobile_device_tokens')) return 0;
        $placeholders = implode(',', array_fill(0, count($userIds), '?'));
        $stmt = Database::connection()->prepare("SELECT COUNT(DISTINCT user_id) FROM mobile_device_tokens WHERE is_active = 1 AND user_id IN ($placeholders)");
        $stmt->execute($userIds);
        return (int)$stmt->fetchColumn();
    }

    public function sendToUser(int $userId, string $subject, string $body, array $data = []): array
    {
        if ($userId <= 0) {
            return ['status' => 'failed', 'sent' => 0, 'failed' => 1, 'error' => 'Invalid recipient.'];
        }

        if (!$this->tableExists('mobile_device_tokens')) {
            return ['status' => 'not_installed', 'sent' => 0, 'failed' => 1, 'error' => 'Native mobile token table is not installed.'];
        }

        $stmt = Database::connection()->prepare('SELECT id, push_token, platform FROM mobile_device_tokens WHERE user_id = ? AND is_active = 1 ORDER BY last_seen_at DESC, id DESC');
        $stmt->execute([$userId]);
        $tokens = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (!$tokens) {
            return ['status' => 'no_device', 'sent' => 0, 'failed' => 0, 'error' => 'No active phone is registered for this member.'];
        }

        $status = $this->configurationStatus();
        if (!$status['configured']) {
            return ['status' => 'not_configured', 'sent' => 0, 'failed' => count($tokens), 'error' => $status['message']];
        }

        $sent = 0;
        $failed = 0;
        $errors = [];
        foreach ($tokens as $row) {
            $platform = strtolower(trim((string)($row['platform'] ?? 'unknown')));
            $result = $this->sendToken((string)$row['push_token'], $platform, $subject, $body, $data);
            if (!empty($result['ok'])) {
                $sent++;
                $this->touchToken((int)$row['id']);
            } else {
                $failed++;
                if (!empty($result['error'])) $errors[] = (string)$result['error'];
                if (!empty($result['deactivate'])) $this->deactivateToken((int)$row['id']);
            }
        }

        return [
            'status' => $sent > 0 ? 'sent' : 'failed',
            'sent' => $sent,
            'failed' => $failed,
            'error' => $sent > 0 ? null : (implode(' | ', array_unique($errors)) ?: 'Push delivery failed.'),
        ];
    }

    private function sendToken(string $token, string $platform, string $subject, string $body, array $data): array
    {
        if ($platform === 'ios') {
            $status = $this->iosConfigurationStatus();
            if (!$status['configured']) return ['ok' => false, 'error' => 'iOS push: ' . $status['message']];
            return $this->sendApnsToken($token, $subject, $body, $data);
        }

        // Android is the expected default for Firebase tokens. Unknown legacy tokens are
        // attempted through Firebase so existing Android registrations remain usable.
        $status = $this->androidConfigurationStatus();
        if (!$status['configured']) return ['ok' => false, 'error' => 'Android push: ' . $status['message']];
        return $this->sendFirebaseToken($token, $subject, $body, $data);
    }

    private function sendFirebaseToken(string $token, string $subject, string $body, array $data): array
    {
        $account = $this->firebaseServiceAccount();
        $projectId = $this->firebaseProjectId($account);
        $accessToken = $this->firebaseAccessToken($account);
        $channel = trim((string)($this->config['default_channel_id'] ?? 'pyrk_general')) ?: 'pyrk_general';

        $payload = [
            'message' => [
                'token' => $token,
                'notification' => [
                    'title' => $this->truncate(trim($subject), 180),
                    'body' => $this->truncate(trim($body), 1000),
                ],
                'data' => $this->cleanData($data),
                'android' => [
                    'priority' => 'high',
                    'notification' => [
                        'channel_id' => $channel,
                        'sound' => 'default',
                    ],
                ],
            ],
        ];

        $response = $this->httpJson(
            'https://fcm.googleapis.com/v1/projects/' . rawurlencode($projectId) . '/messages:send',
            $payload,
            ['Authorization: Bearer ' . $accessToken]
        );

        if ($response['status'] >= 200 && $response['status'] < 300) {
            return ['ok' => true, 'name' => (string)($response['json']['name'] ?? '')];
        }

        $errorText = (string)($response['json']['error']['message'] ?? ('Firebase HTTP ' . $response['status']));
        $errorCode = strtoupper((string)($response['json']['error']['status'] ?? ''));
        $details = $response['json']['error']['details'] ?? [];
        foreach (is_array($details) ? $details : [] as $detail) {
            if (!empty($detail['errorCode'])) $errorCode .= ' ' . strtoupper((string)$detail['errorCode']);
        }
        $upperError = strtoupper($errorText);
        $deactivate = strpos($errorCode, 'UNREGISTERED') !== false || strpos($upperError, 'NOT REGISTERED') !== false;

        return ['ok' => false, 'error' => $errorText, 'deactivate' => $deactivate];
    }

    private function sendApnsToken(string $token, string $subject, string $body, array $data): array
    {
        $ios = $this->iosConfig();
        $environment = strtolower(trim((string)($ios['environment'] ?? 'production')));
        $host = $environment === 'sandbox' ? 'https://api.sandbox.push.apple.com' : 'https://api.push.apple.com';
        $bundleId = trim((string)($ios['bundle_id'] ?? 'com.thepyrk.app'));
        $cleanToken = preg_replace('/[^a-fA-F0-9]/', '', $token) ?: '';
        if (strlen($cleanToken) < 32) {
            return ['ok' => false, 'error' => 'APNs device token is invalid.', 'deactivate' => true];
        }

        $payload = array_merge([
            'aps' => [
                'alert' => [
                    'title' => $this->truncate(trim($subject), 180),
                    'body' => $this->truncate(trim($body), 1000),
                ],
                'sound' => 'default',
            ],
        ], $this->cleanData($data));

        $ch = curl_init($host . '/3/device/' . rawurlencode($cleanToken));
        $headers = [
            'authorization: bearer ' . $this->apnsJwt(),
            'apns-topic: ' . $bundleId,
            'apns-push-type: alert',
            'apns-priority: 10',
            'apns-expiration: 0',
            'content-type: application/json; charset=utf-8',
        ];
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_POSTFIELDS => $this->jsonEncode($payload),
            CURLOPT_HEADER => false,
        ]);
        $raw = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($raw !== false && $status >= 200 && $status < 300) {
            return ['ok' => true];
        }

        $decoded = json_decode((string)$raw, true);
        $reason = (string)($decoded['reason'] ?? ($curlError ?: ('APNs HTTP ' . $status)));
        $deactivateReasons = ['BadDeviceToken', 'DeviceTokenNotForTopic', 'Unregistered'];
        return ['ok' => false, 'error' => $reason, 'deactivate' => in_array($reason, $deactivateReasons, true)];
    }

    private function androidConfigurationStatus(): array
    {
        if (empty($this->config['enabled'])) {
            return ['configured' => false, 'message' => 'disabled'];
        }

        $android = $this->androidConfig();
        if (array_key_exists('enabled', $android) && empty($android['enabled'])) {
            return ['configured' => false, 'message' => 'disabled'];
        }
        $path = trim((string)($android['service_account_json'] ?? ''));
        if ($path === '' || !is_file($path) || !is_readable($path)) {
            return ['configured' => false, 'message' => 'Firebase service-account JSON is missing or unreadable.'];
        }
        if (!function_exists('curl_init') || !function_exists('openssl_sign')) {
            return ['configured' => false, 'message' => 'PHP cURL and OpenSSL are required.'];
        }

        try {
            $account = $this->firebaseServiceAccount();
            if ($this->firebaseProjectId($account) === '') {
                return ['configured' => false, 'message' => 'Firebase project ID is missing.'];
            }
            if (empty($account['client_email']) || empty($account['private_key'])) {
                return ['configured' => false, 'message' => 'Firebase service-account JSON is incomplete.'];
            }
        } catch (Throwable $e) {
            return ['configured' => false, 'message' => $e->getMessage()];
        }

        return ['configured' => true, 'message' => 'ready'];
    }

    private function iosConfigurationStatus(): array
    {
        if (empty($this->config['enabled'])) {
            return ['configured' => false, 'message' => 'disabled'];
        }

        $ios = $this->iosConfig();
        if (array_key_exists('enabled', $ios) && empty($ios['enabled'])) {
            return ['configured' => false, 'message' => 'disabled'];
        }
        if (!function_exists('curl_init') || !function_exists('openssl_sign')) {
            return ['configured' => false, 'message' => 'PHP cURL and OpenSSL are required.'];
        }
        if (!defined('CURL_HTTP_VERSION_2_0')) {
            return ['configured' => false, 'message' => 'PHP cURL HTTP/2 support is required for APNs.'];
        }

        $required = [
            'team_id' => 'Apple Team ID',
            'key_id' => 'APNs Key ID',
            'bundle_id' => 'iOS bundle ID',
            'auth_key_p8' => 'APNs .p8 key path',
        ];
        foreach ($required as $key => $label) {
            if (trim((string)($ios[$key] ?? '')) === '') {
                return ['configured' => false, 'message' => $label . ' is missing.'];
            }
        }
        $keyPath = trim((string)$ios['auth_key_p8']);
        if (!is_file($keyPath) || !is_readable($keyPath)) {
            return ['configured' => false, 'message' => 'APNs .p8 key is missing or unreadable.'];
        }

        return ['configured' => true, 'message' => 'ready'];
    }

    private function androidConfig(): array
    {
        $nested = $this->config['android'] ?? [];
        return [
            'enabled' => $nested['enabled'] ?? true,
            'firebase_project_id' => $nested['firebase_project_id'] ?? ($this->config['firebase_project_id'] ?? ''),
            'service_account_json' => $nested['service_account_json'] ?? ($this->config['service_account_json'] ?? ''),
        ];
    }

    private function iosConfig(): array
    {
        $nested = $this->config['ios'] ?? [];
        return [
            'enabled' => $nested['enabled'] ?? false,
            'team_id' => $nested['team_id'] ?? ($this->config['apns_team_id'] ?? ''),
            'key_id' => $nested['key_id'] ?? ($this->config['apns_key_id'] ?? ''),
            'bundle_id' => $nested['bundle_id'] ?? ($this->config['apns_bundle_id'] ?? 'com.thepyrk.app'),
            'auth_key_p8' => $nested['auth_key_p8'] ?? ($this->config['apns_auth_key_p8'] ?? ''),
            'environment' => $nested['environment'] ?? ($this->config['apns_environment'] ?? 'production'),
        ];
    }

    private function firebaseServiceAccount(): array
    {
        $path = trim((string)($this->androidConfig()['service_account_json'] ?? ''));
        if ($path === '' || !is_file($path) || !is_readable($path)) {
            throw new RuntimeException('Firebase service-account JSON is missing or unreadable.');
        }
        $decoded = json_decode((string)file_get_contents($path), true);
        if (!is_array($decoded)) throw new RuntimeException('Firebase service-account JSON is invalid.');
        return $decoded;
    }

    private function firebaseProjectId(array $account): string
    {
        return trim((string)($this->androidConfig()['firebase_project_id'] ?? $account['project_id'] ?? ''));
    }

    private function firebaseAccessToken(array $account): string
    {
        if (self::$cachedFirebaseAccessToken && (int)(self::$cachedFirebaseAccessToken['expires_at'] ?? 0) > time() + 60) {
            return (string)self::$cachedFirebaseAccessToken['token'];
        }

        $now = time();
        $tokenUri = trim((string)($account['token_uri'] ?? 'https://oauth2.googleapis.com/token'));
        $header = $this->base64Url($this->jsonEncode(['alg' => 'RS256', 'typ' => 'JWT']));
        $claims = $this->base64Url($this->jsonEncode([
            'iss' => (string)$account['client_email'],
            'scope' => 'https://www.googleapis.com/auth/firebase.messaging',
            'aud' => $tokenUri,
            'iat' => $now,
            'exp' => $now + 3600,
        ]));
        $unsigned = $header . '.' . $claims;
        $signature = '';
        if (!openssl_sign($unsigned, $signature, (string)$account['private_key'], OPENSSL_ALGO_SHA256)) {
            throw new RuntimeException('Could not sign Firebase OAuth request.');
        }
        $assertion = $unsigned . '.' . $this->base64Url($signature);

        $ch = curl_init($tokenUri);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 25,
            CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
            CURLOPT_POSTFIELDS => http_build_query([
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                'assertion' => $assertion,
            ]),
        ]);
        $raw = curl_exec($ch);
        $http = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        $json = json_decode((string)$raw, true);
        if ($raw === false || $http < 200 || $http >= 300 || empty($json['access_token'])) {
            throw new RuntimeException((string)($json['error_description'] ?? ($curlError ?: 'Could not obtain Firebase access token.')));
        }

        self::$cachedFirebaseAccessToken = [
            'token' => (string)$json['access_token'],
            'expires_at' => $now + max(300, (int)($json['expires_in'] ?? 3600)),
        ];
        return (string)self::$cachedFirebaseAccessToken['token'];
    }

    private function apnsJwt(): string
    {
        if (self::$cachedApnsJwt && (int)(self::$cachedApnsJwt['expires_at'] ?? 0) > time() + 60) {
            return (string)self::$cachedApnsJwt['token'];
        }

        $ios = $this->iosConfig();
        $now = time();
        $header = $this->base64Url($this->jsonEncode(['alg' => 'ES256', 'kid' => (string)$ios['key_id']]));
        $claims = $this->base64Url($this->jsonEncode(['iss' => (string)$ios['team_id'], 'iat' => $now]));
        $unsigned = $header . '.' . $claims;
        $derSignature = '';
        $privateKey = (string)file_get_contents((string)$ios['auth_key_p8']);
        if (!openssl_sign($unsigned, $derSignature, $privateKey, OPENSSL_ALGO_SHA256)) {
            throw new RuntimeException('Could not sign APNs authentication token.');
        }
        $rawSignature = $this->ecdsaDerToJose($derSignature, 64);
        $token = $unsigned . '.' . $this->base64Url($rawSignature);

        // APNs provider tokens are valid for one hour. Refresh at 50 minutes.
        self::$cachedApnsJwt = ['token' => $token, 'expires_at' => $now + 3000];
        return $token;
    }

    private function ecdsaDerToJose(string $der, int $outputLength): string
    {
        $offset = 0;
        if (ord($der[$offset++] ?? "\0") !== 0x30) throw new RuntimeException('Invalid APNs signature sequence.');
        $this->readDerLength($der, $offset);
        if (ord($der[$offset++] ?? "\0") !== 0x02) throw new RuntimeException('Invalid APNs signature integer.');
        $rLength = $this->readDerLength($der, $offset);
        $r = substr($der, $offset, $rLength);
        $offset += $rLength;
        if (ord($der[$offset++] ?? "\0") !== 0x02) throw new RuntimeException('Invalid APNs signature integer.');
        $sLength = $this->readDerLength($der, $offset);
        $s = substr($der, $offset, $sLength);

        $partLength = intdiv($outputLength, 2);
        $r = str_pad(ltrim($r, "\0"), $partLength, "\0", STR_PAD_LEFT);
        $s = str_pad(ltrim($s, "\0"), $partLength, "\0", STR_PAD_LEFT);
        if (strlen($r) !== $partLength || strlen($s) !== $partLength) {
            throw new RuntimeException('Invalid APNs signature length.');
        }
        return $r . $s;
    }

    private function readDerLength(string $der, int &$offset): int
    {
        $length = ord($der[$offset++] ?? "\0");
        if (($length & 0x80) === 0) return $length;
        $bytes = $length & 0x7f;
        if ($bytes < 1 || $bytes > 4) throw new RuntimeException('Invalid APNs DER length.');
        $length = 0;
        for ($i = 0; $i < $bytes; $i++) {
            $length = ($length << 8) | ord($der[$offset++] ?? "\0");
        }
        return $length;
    }

    private function cleanData(array $data): array
    {
        $clean = [];
        foreach ($data as $key => $value) {
            if (!is_scalar($value) && $value !== null) continue;
            $safeKey = preg_replace('/[^a-zA-Z0-9_.-]/', '_', (string)$key) ?: 'data';
            $clean[$safeKey] = $this->truncate((string)$value, 1000);
        }
        return $clean;
    }

    private function httpJson(string $url, array $payload, array $headers = []): array
    {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json; charset=utf-8'], $headers),
            CURLOPT_POSTFIELDS => $this->jsonEncode($payload),
        ]);
        $raw = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($raw === false) return ['status' => 0, 'json' => ['error' => ['message' => $error ?: 'Firebase request failed.']]];
        $json = json_decode((string)$raw, true);
        return ['status' => $status, 'json' => is_array($json) ? $json : []];
    }

    private function jsonEncode(array $value): string
    {
        $encoded = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($encoded === false) throw new RuntimeException('Could not encode push notification payload.');
        return $encoded;
    }

    private function truncate(string $value, int $max): string
    {
        if (function_exists('mb_substr')) return mb_substr($value, 0, $max, 'UTF-8');
        return substr($value, 0, $max);
    }

    private function base64Url(string $value): string
    {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }

    private function cleanIds(array $ids): array
    {
        return array_values(array_unique(array_filter(array_map('intval', $ids), static fn(int $id): bool => $id > 0)));
    }

    private function tableExists(string $table): bool
    {
        static $cache = [];
        if (array_key_exists($table, $cache)) return $cache[$table];
        try {
            $stmt = Database::connection()->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?');
            $stmt->execute([$table]);
            return $cache[$table] = ((int)$stmt->fetchColumn() > 0);
        } catch (Throwable) {
            return $cache[$table] = false;
        }
    }

    private function touchToken(int $id): void
    {
        try {
            Database::connection()->prepare('UPDATE mobile_device_tokens SET last_seen_at = NOW(), updated_at = NOW() WHERE id = ?')->execute([$id]);
        } catch (Throwable) {}
    }

    private function deactivateToken(int $id): void
    {
        try {
            Database::connection()->prepare('UPDATE mobile_device_tokens SET is_active = 0, updated_at = NOW() WHERE id = ?')->execute([$id]);
        } catch (Throwable) {}
    }
}
