<?php

namespace App\Services\License;

use App\Models\ApiRequestLog;
use App\Models\License;
use App\Models\LicenseActivation;
use App\Services\Security\AuditService;
use App\Services\Security\SignatureService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class LicenseValidationService
{
    private const BRUTE_FORCE_THRESHOLD = 10;
    private const BRUTE_FORCE_WINDOW    = 300; // 5 minutes

    public function __construct(
        private SignatureService $signer,
        private AuditService $audit,
    ) {}

    /**
     * Activate a license for a domain/installation.
     */
    public function activate(array $data): array
    {
        if ($block = $this->checkBruteForce($data['license_key'] ?? '', $data['ip'] ?? '')) {
            return $block;
        }

        $license = $this->findAndValidateLicense($data['license_key'] ?? '');

        if (! $license) {
            $this->recordFailure($data, 'invalid_license');
            return $this->errorResponse('Invalid or inactive license key.', 'INVALID_LICENSE');
        }

        // Signature verification
        if (! $this->signer->verifyRequest($data, $license->secret_key)) {
            $this->audit->securityEvent('invalid_signature', "Invalid signature for license {$license->license_key}", 'high', [
                'license_key' => $license->license_key,
                'ip_address'  => $data['ip'] ?? null,
            ]);
            $this->recordFailure($data, 'invalid_signature');
            return $this->errorResponse('Request signature invalid.', 'INVALID_SIGNATURE');
        }

        // Check activation slots
        if (! $license->hasActivationSlots()) {
            $this->recordFailure($data, 'max_activations_reached');
            return $this->errorResponse('Maximum activations reached.', 'MAX_ACTIVATIONS');
        }

        // Domain validation
        if ($license->product->allow_domain_activation && isset($data['domain'])) {
            if (! $this->validateDomain($data['domain'])) {
                $this->recordFailure($data, 'invalid_domain');
                return $this->errorResponse('Invalid domain format.', 'INVALID_DOMAIN');
            }
        }

        // Check for existing active activation on same domain/install
        $existing = LicenseActivation::where('license_id', $license->id)
            ->where('status', 'active')
            ->when(isset($data['domain']), fn($q) => $q->where('domain', $data['domain']))
            ->when(isset($data['installation_id']), fn($q) => $q->orWhere('installation_id', $data['installation_id']))
            ->first();

        if ($existing) {
            // Return the existing activation token (idempotent)
            return $this->successActivationResponse($license, $existing, 'already_active');
        }

        return DB::transaction(function () use ($license, $data) {
            $activationToken = bin2hex(random_bytes(32));
            $activationId    = Str::uuid()->toString();

            $checksumData = [
                'license_id'     => $license->id,
                'activation_id'  => $activationId,
                'domain'         => $data['domain'] ?? null,
                'installation_id'=> $data['installation_id'] ?? null,
            ];

            $activation = LicenseActivation::create([
                'license_id'          => $license->id,
                'activation_id'       => $activationId,
                'domain'              => $data['domain'] ?? null,
                'ip_address'          => $data['ip'] ?? null,
                'installation_id'     => $data['installation_id'] ?? null,
                'machine_fingerprint' => $data['machine_fingerprint'] ?? null,
                'server_hostname'     => $data['server_hostname'] ?? null,
                'os_platform'         => $data['os_platform'] ?? null,
                'status'              => 'active',
                'activated_at'        => now(),
                'product_version'     => $data['product_version'] ?? null,
                'activation_token'    => $activationToken,
                'activation_checksum' => $this->signer->computeLicenseChecksum($checksumData, $license->secret_key),
            ]);

            $license->increment('activations_count');

            if (! $license->activated_at) {
                $license->update(['activated_at' => now()]);
            }

            $this->audit->log('license.activated', $license, [], ['domain' => $data['domain'] ?? null]);
            $this->recordSuccess($data, 'activate');

            return $this->successActivationResponse($license, $activation, 'activated');
        });
    }

    /**
     * Validate (check) a license without changing activation state.
     */
    public function validate(array $data): array
    {
        if ($block = $this->checkBruteForce($data['license_key'] ?? '', $data['ip'] ?? '')) {
            return $block;
        }

        $license = $this->findAndValidateLicense($data['license_key'] ?? '');

        if (! $license) {
            $this->recordFailure($data, 'invalid_license');
            return $this->errorResponse('Invalid license.', 'INVALID_LICENSE');
        }

        if (! $this->signer->verifyRequest($data, $license->secret_key)) {
            $this->audit->securityEvent('invalid_signature', "Validation signature mismatch for {$license->license_key}", 'high');
            $this->recordFailure($data, 'invalid_signature');
            return $this->errorResponse('Invalid signature.', 'INVALID_SIGNATURE');
        }

        // Find matching activation
        $activation = LicenseActivation::where('license_id', $license->id)
            ->where('status', 'active')
            ->when(isset($data['activation_token']), fn($q) => $q->where('activation_token', $data['activation_token']))
            ->when(isset($data['domain']), fn($q) => $q->where('domain', $data['domain']))
            ->first();

        if (! $activation) {
            $this->recordFailure($data, 'not_activated');
            return $this->errorResponse('No matching active activation found.', 'NOT_ACTIVATED');
        }

        // Verify activation integrity
        $checksumData = [
            'license_id'      => $license->id,
            'activation_id'   => $activation->activation_id,
            'domain'          => $activation->domain,
            'installation_id' => $activation->installation_id,
        ];
        if (! $this->signer->verifyLicenseChecksum($checksumData, $activation->activation_checksum, $license->secret_key)) {
            $this->audit->logTamperDetected($license->license_key, $data['ip'] ?? '');
            $this->recordFailure($data, 'tamper_detected');
            return $this->errorResponse('Activation integrity check failed.', 'TAMPER_DETECTED');
        }

        $activation->update(['last_validated_at' => now()]);
        $this->recordSuccess($data, 'validate');

        return $this->signer->signResponse([
            'valid'          => true,
            'license_key'    => $license->license_key,
            'status'         => $license->status,
            'type'           => $license->type,
            'product'        => $license->product->name,
            'expires_at'     => $license->expires_at?->toIso8601String(),
            'is_lifetime'    => is_null($license->expires_at),
            'activation_id'  => $activation->activation_id,
        ], $license->secret_key);
    }

    /**
     * Deactivate a license activation.
     */
    public function deactivate(array $data): array
    {
        $license = $this->findAndValidateLicense($data['license_key'] ?? '', allowExpired: true);

        if (! $license || ! $this->signer->verifyRequest($data, $license->secret_key)) {
            return $this->errorResponse('Invalid request.', 'INVALID_REQUEST');
        }

        $activation = LicenseActivation::where('license_id', $license->id)
            ->where('status', 'active')
            ->when(isset($data['activation_token']), fn($q) => $q->where('activation_token', $data['activation_token']))
            ->when(isset($data['domain']), fn($q) => $q->where('domain', $data['domain']))
            ->first();

        if (! $activation) {
            return $this->errorResponse('No active activation found.', 'NOT_FOUND');
        }

        DB::transaction(function () use ($license, $activation) {
            $activation->update(['status' => 'deactivated', 'deactivated_at' => now()]);
            $license->decrement('activations_count');
            $this->audit->log('license.deactivated', $license);
        });

        return $this->signer->signResponse(['success' => true, 'message' => 'Activation removed.'], $license->secret_key);
    }

    /**
     * Heartbeat — keep activation alive and detect stale activations.
     */
    public function heartbeat(array $data): array
    {
        $license = $this->findAndValidateLicense($data['license_key'] ?? '');
        if (! $license || ! $this->signer->verifyRequest($data, $license->secret_key)) {
            return $this->errorResponse('Invalid request.', 'INVALID_REQUEST');
        }

        $activation = LicenseActivation::where('license_id', $license->id)
            ->where('activation_token', $data['activation_token'] ?? '')
            ->where('status', 'active')
            ->first();

        if (! $activation) {
            return $this->errorResponse('Activation not found.', 'NOT_FOUND');
        }

        $activation->update(['last_heartbeat_at' => now()]);

        return $this->signer->signResponse([
            'alive'      => true,
            'expires_at' => $license->expires_at?->toIso8601String(),
            'server_time'=> now()->toIso8601String(),
        ], $license->secret_key);
    }

    /**
     * Check for available updates.
     */
    public function checkUpdate(array $data): array
    {
        $license = $this->findAndValidateLicense($data['license_key'] ?? '');
        if (! $license || ! $this->signer->verifyRequest($data, $license->secret_key)) {
            return $this->errorResponse('Invalid request.', 'INVALID_REQUEST');
        }

        $product     = $license->product;
        $latest      = $product->latestVersion;
        $currentVer  = $data['current_version'] ?? '0.0.0';

        $hasUpdate = $latest && version_compare($latest->version, $currentVer, '>');

        $response = [
            'update_available' => $hasUpdate,
            'current_version'  => $currentVer,
            'latest_version'   => $latest?->version,
            'changelog'        => $hasUpdate ? $latest->changelog : null,
            'download_url'     => $hasUpdate ? route('api.license.download', ['key' => $license->license_key]) : null,
            'checksum'         => $hasUpdate ? $latest->checksum : null,
        ];

        return $this->signer->signResponse($response, $license->secret_key);
    }

    // ----------------------------------------------------------------
    // Private helpers
    // ----------------------------------------------------------------

    private function findAndValidateLicense(string $key, bool $allowExpired = false): ?License
    {
        if (empty($key)) return null;

        $license = License::with('product')
            ->where('license_key', $key)
            ->first();

        if (! $license) return null;

        if (in_array($license->status, ['revoked', 'cancelled', 'suspended'])) {
            return null;
        }

        if (! $allowExpired && $license->isExpired()) {
            return null;
        }

        return $license;
    }

    private function checkBruteForce(string $licenseKey, string $ip): ?array
    {
        $key   = "bf:{$ip}";
        $count = Cache::get($key, 0);

        if ($count >= self::BRUTE_FORCE_THRESHOLD) {
            $this->audit->logBruteForce($licenseKey, $ip);

            // Log so it's visible in API Monitor
            ApiRequestLog::create([
                'endpoint'       => request()->path(),
                'method'         => request()->method(),
                'license_key'    => $licenseKey,
                'ip_address'     => $ip,
                'domain'         => request()->input('domain'),
                'response_code'  => 429,
                'success'        => false,
                'failure_reason' => 'brute_force_blocked',
                'nonce'          => request()->input('nonce'),
                'created_at'     => now(),
            ]);

            // Return structured error instead of abort() so Nexus gets clear JSON
            return [
                'success' => false,
                'error'   => 'BRUTE_FORCE_BLOCKED',
                'message' => 'Too many failed activation attempts. Your IP has been temporarily blocked.',
                'hint'    => 'Wait 5 minutes before trying again. This protection activates after ' . self::BRUTE_FORCE_THRESHOLD . ' consecutive failures. If you believe this is an error, contact your software vendor.',
            ];
        }

        return null; // not blocked
    }

    private function incrementBruteForce(string $ip): void
    {
        $key = "bf:{$ip}";
        Cache::put($key, Cache::get($key, 0) + 1, self::BRUTE_FORCE_WINDOW);
    }

    private function validateDomain(string $domain): bool
    {
        $domain = preg_replace('#^https?://#', '', $domain);
        $domain = explode('/', $domain)[0];
        return (bool) preg_match('/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/', $domain)
            || filter_var($domain, FILTER_VALIDATE_IP);
    }

    private function successActivationResponse(License $license, LicenseActivation $activation, string $message): array
    {
        return $this->signer->signResponse([
            'success'        => true,
            'message'        => $message,
            'license_key'    => $license->license_key,
            'activation_id'  => $activation->activation_id,
            'activation_token'=> $activation->activation_token,
            'status'         => $license->status,
            'type'           => $license->type,
            'product'        => $license->product->name,
            'expires_at'     => $license->expires_at?->toIso8601String(),
            'is_lifetime'    => is_null($license->expires_at),
            'max_activations'=> $license->max_activations,
            'activations_used'=> $license->activations_count,
        ], $license->secret_key);
    }

    private function errorResponse(string $message, string $code): array
    {
        $hints = [
            'INVALID_LICENSE'    => 'The license key does not exist, has been revoked, suspended, or has expired. Check the key is entered correctly and contact your vendor if the problem persists.',
            'INVALID_SIGNATURE'  => 'The Secret Key is wrong, or your server clock is out of sync (must be within 5 minutes of the License Platform). Double-check the Secret Key from your customer portal.',
            'MAX_ACTIVATIONS'    => 'All activation slots for this license are in use. Log into your customer portal, go to Licenses, and remove an existing installation to free up a slot.',
            'INVALID_DOMAIN'     => 'The domain sent with this request is not a valid hostname or IP address. Check the domain configuration in your software settings.',
            'NOT_ACTIVATED'      => 'No active activation was found for this license on this server. Run the activation step again or contact your vendor.',
            'TAMPER_DETECTED'    => 'The local activation record has been modified or corrupted. Delete the stored activation file/database entry and re-activate the license.',
            'NOT_FOUND'          => 'The activation token was not found or has already been deactivated. Re-activate the license to get a new token.',
            'INVALID_REQUEST'    => 'The request is missing required fields or contains invalid data. Ensure timestamp, nonce, and signature are all included and correctly computed.',
        ];

        return [
            'success' => false,
            'error'   => $code,
            'message' => $message,
            'hint'    => $hints[$code] ?? 'Contact the software vendor for assistance.',
        ];
    }

    private function recordFailure(array $data, string $reason): void
    {
        // Increment brute-force counter only on actual failures (not every request)
        $this->incrementBruteForce($data['ip'] ?? request()->ip());

        ApiRequestLog::create([
            'endpoint'       => request()->path(),
            'method'         => request()->method(),
            'license_key'    => $data['license_key'] ?? null,
            'ip_address'     => $data['ip'] ?? request()->ip(),
            'domain'         => $data['domain'] ?? null,
            'response_code'  => 422,
            'success'        => false,
            'failure_reason' => $reason,
            'nonce'          => $data['nonce'] ?? null,
            'created_at'     => now(),
        ]);
    }

    private function recordSuccess(array $data, string $action): void
    {
        // If domain not in request (e.g. validate/heartbeat use activation_token),
        // look it up from the activation record so API Monitor always shows domain.
        $domain = $data['domain'] ?? null;
        if (! $domain && isset($data['activation_token'])) {
            $domain = \App\Models\LicenseActivation::where('activation_token', $data['activation_token'])
                ->value('domain');
        }

        ApiRequestLog::create([
            'endpoint'     => request()->path(),
            'method'       => request()->method(),
            'license_key'  => $data['license_key'] ?? null,
            'ip_address'   => $data['ip'] ?? request()->ip(),
            'domain'       => $domain,
            'response_code'=> 200,
            'success'      => true,
            'nonce'        => $data['nonce'] ?? null,
            'created_at'   => now(),
        ]);
    }
}
