<?php

namespace Licenzo\Services;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

use Licenzo\App;
use Licenzo\Helper\Action;
use Licenzo\Model\Customer;
use Licenzo\Model\License;
use Licenzo\Model\LicenseActivation;
use Licenzo\Model\LicenseProduct;
use Licenzo\Model\Product;
use Licenzo\Model\ProductVariation;
use Licenzo\Framework\Support\Facades\DB;
use \Licenzo\Services\Logging\LicenseEvent;

class LicenseService
{
    private ActivationTokenService $tokenService;

    public function __construct(?ActivationTokenService $tokenService = null)
    {
        $this->tokenService = $tokenService ?? new ActivationTokenService();
    }

    // Activate a license
    public function activateLicense(License $license, Product $product, ProductVariation $variation, array $data): ?LicenseActivation
    {
        $instanceIdentifier = $data['instance_identifier'] ?? 'unknown';

        // STEP 1: Check license status
        if ($license->status !== License::STATUS_ACTIVE) {
            throw new \Exception(esc_html__('License is not active.', 'licenzo'));
        }

        // STEP 2: Validate license-product association (moved up before activation logic)
        $licenseProduct = LicenseProduct::where('license_id', $license->id)
            ->where('product_id', $product->id)
            ->where('variation_id', $variation->id)
            ->first();

        if (!$licenseProduct) {
            $this->log('error', $license, 'activation_failed', null, 'Product not associated with this license', [
                'product_id' => $product->id,
                'variation_id' => $variation->id,
            ]);
            throw new \Exception(esc_html__('Product not associated with this license.', 'licenzo'));
        }

        // STEP 3: Same instance already ACTIVE → idempotent return (same device requesting again)
        $existingActive = LicenseActivation::where('license_id', $license->id)
            ->where('product_id', $product->id)
            ->where('variation_id', $variation->id)
            ->where('instance_identifier', $instanceIdentifier)
            ->where('status', LicenseActivation::STATUS_ACTIVE)
            ->first();

        if ($existingActive) {
            return $existingActive;
        }

        // STEP 4: Same instance previously DEACTIVATED → reactivate (bypasses activation limit)
        // This is the "old fingerprint" case: a device that was previously authorized
        // gets its activation back even if the limit is now full with other devices.
        $deactivatedSameInstance = LicenseActivation::where('license_id', $license->id)
            ->where('product_id', $product->id)
            ->where('variation_id', $variation->id)
            ->where('instance_identifier', $instanceIdentifier)
            ->where('status', LicenseActivation::STATUS_DEACTIVATED)
            ->first();

        if ($deactivatedSameInstance) {
            DB::beginTransaction();
            try {
                $deactivatedSameInstance->status = LicenseActivation::STATUS_ACTIVE;
                $deactivatedSameInstance->activated_at = gmdate('Y-m-d H:i:s');
                $deactivatedSameInstance->deactivated_at = null;
                $deactivatedSameInstance->ip_address = App::request()->ip() ?? null;
                $deactivatedSameInstance->user_agent = App::request()->userAgent() ?? null;
                $deactivatedSameInstance->activation_method = $data['activation_method'] ?? LicenseActivation::METHOD_KEY_BASED;
                $deactivatedSameInstance->save();

                $fingerprint = $data['fingerprint'] ?? $instanceIdentifier;
                $deactivatedSameInstance->activation_hash = $this->tokenService->generate(
                    $license->id,
                    $deactivatedSameInstance->id,
                    $product->id,
                    $variation->id,
                    $fingerprint
                );
                $deactivatedSameInstance->save();

                $license->activation_count += 1;
                $license->save();
                $licenseProduct->activation_count += 1;
                $licenseProduct->save();

                $this->log('success', $license, 'reactivated', $deactivatedSameInstance, 'License reactivated on previously authorized device', [
                    'activation_hash' => $deactivatedSameInstance->activation_hash,
                ]);

                DB::commit();
            } catch (\Throwable $e) {
                DB::rollback();
                throw $e;
            }

            $this->dispatchLicenseActivationNotification($license, $product, $variation, $deactivatedSameInstance, 'reactivated');

            return $deactivatedSameInstance;
        }

        // STEP 5: Enforce activation limit (only for genuinely NEW devices)
        if (
            $license->activation_limit > 0 &&
            $license->activation_count >= $license->activation_limit
        ) {
            $this->log('error', $license, 'activation_failed', null, 'Activation limit reached', [
                'activation_limit' => $license->activation_limit,
            ]);

            $this->dispatchMaximumActivationsNotification($license, $product, $variation, $instanceIdentifier);

            throw new \Exception(sprintf(
            /* translators: %d: activation limit */
                esc_html__('Activation limit reached (%d)', 'licenzo'),
                esc_html($license->activation_limit)
            ));
        }

        // STEP 6: Create new activation
        DB::beginTransaction();
        try {
            $activation = LicenseActivation::create([
                'license_id' => $license->id,
                'product_id' => $product->id,
                'variation_id' => $variation->id,
                'product_name' => $product->name,
                'variation_name' => $variation->name,
                'instance_identifier' => $instanceIdentifier,
                'activation_hash' => '',
                'status' => LicenseActivation::STATUS_ACTIVE,
                'ip_address' => App::request()->ip() ?? null,
                'user_agent' => App::request()->userAgent() ?? null,
                'activated_at' => gmdate('Y-m-d H:i:s'),
                'activation_method' => $data['activation_method'] ?? LicenseActivation::METHOD_KEY_BASED,
                'expire_date' => $license->expire_date ? gmdate('Y-m-d', strtotime($license->expire_date)) : null,
                'meta' => isset($data['meta']) ? wp_json_encode($data['meta']) : null,
            ]);

            $fingerprint = $data['fingerprint'] ?? $instanceIdentifier;
            $activation->activation_hash = $this->tokenService->generate(
                $license->id,
                $activation->id,
                $product->id,
                $variation->id,
                $fingerprint
            );
            $activation->save();

            $license->activation_count += 1;
            $license->save();
            $licenseProduct->activation_count += 1;
            $licenseProduct->save();

            $this->log('success', $license, 'activated', $activation, 'License activated successfully', [
                'activation_hash' => $activation->activation_hash,
                'valid_till' => $license->expire_date,
                'status' => $license->status,
            ]);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();
            throw $e;
        }

        $this->dispatchLicenseActivationNotification($license, $product, $variation, $activation, 'activated');

        return $activation;
    }



    // Deactivate a license
    public function deactivateLicense(License $license, Product $product, ProductVariation $variation, array $data): bool
    {
        // Find the activation record
        $activation = LicenseActivation::where('license_id', $license->id)
            ->where('product_id', $product->id)
            ->where('variation_id', $variation->id)
            ->where('instance_identifier', $data['instance_identifier'] ?? 'unknown')
            ->first();

        if (!$activation || $activation->status !== LicenseActivation::STATUS_ACTIVE) {
            return false;
        }

        DB::beginTransaction();
        try {
            $activation->status = LicenseActivation::STATUS_DEACTIVATED;
            $activation->deactivated_at = gmdate('Y-m-d H:i:s');
            $activation->save();

            $license->activation_count = max(0, $license->activation_count - 1);
            $license->save();

            $licenseProduct = LicenseProduct::where('license_id', $license->id)
                ->where('product_id', $product->id)
                ->where('variation_id', $variation->id)
                ->first();
            if ($licenseProduct) {
                $licenseProduct->activation_count = max(0, $licenseProduct->activation_count - 1);
                $licenseProduct->save();
            }

            $this->log('success', $license, 'deactivated', $activation, 'License deactivated successfully', [
                'deactivated_at' => $activation->deactivated_at,
            ]);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();
            throw $e;
        }

        return true;
    }

    /**
     * Deactivate a specific activation by its ID.
     *
     * Used for customer self-service deactivation from the WooCommerce account panel.
     * Validates that the activation belongs to the given license before deactivating.
     *
     * @param License $license
     * @param int $activationId
     * @return bool
     * @throws \Exception
     */
    public function deactivateActivationById(License $license, int $activationId): bool
    {
        $activation = LicenseActivation::where('id', $activationId)
            ->where('license_id', $license->id)
            ->first();

        if (!$activation) {
            throw new \Exception(esc_html__('Activation not found for this license.', 'licenzo'));
        }

        if ($activation->status !== LicenseActivation::STATUS_ACTIVE) {
            throw new \Exception(esc_html__('Activation is not active.', 'licenzo'));
        }

        DB::beginTransaction();
        try {
            $activation->status = LicenseActivation::STATUS_DEACTIVATED;
            $activation->deactivated_at = gmdate('Y-m-d H:i:s');
            $activation->save();

            $license->activation_count = max(0, $license->activation_count - 1);
            $license->save();

            $licenseProduct = LicenseProduct::where('license_id', $license->id)
                ->where('product_id', $activation->product_id)
                ->where('variation_id', $activation->variation_id)
                ->first();

            if ($licenseProduct) {
                $licenseProduct->activation_count = max(0, $licenseProduct->activation_count - 1);
                $licenseProduct->save();
            }

            $this->log('success', $license, 'deactivated', $activation, 'Activation deactivated by customer', [
                'activation_id' => $activationId,
                'deactivated_at' => $activation->deactivated_at,
            ]);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();
            throw $e;
        }

        return true;
    }

    /**
     * Check license validity for the given product/variation and instance data.
     *
     * Returns the validated LicenseActivation on success, or null on failure.
     */
    public function checkLicense(License $license, Product $product, ProductVariation $variation, array $data): ?LicenseActivation
    {
        $activation = LicenseActivation::where('license_id', $license->id)
            ->where('product_id', $product->id)
            ->where('variation_id', $variation->id)
            ->where('instance_identifier', $data['instance_identifier'] ?? 'unknown')
            ->first();

        if ($license->status !== License::STATUS_ACTIVE) {
            $this->log('error', $license, 'validation_failed', null, 'License is not active', $data ?? []);
            return null;
        }

        // Uses > (not >=) because the activation being validated is already counted in activation_count.
        // A limit of 2 with count 2 means both activations are valid; count 3 means one is over the limit.
        if ($license->activation_limit > 0 && $license->activation_count > $license->activation_limit) {
            $this->log('error', $license, 'validation_failed', null, 'Activation limit reached', $data ?? []);
            return null;
        }

        if ($license->expire_date && strtotime($license->expire_date) < time()) {
            $this->log('error', $license, 'validation_failed', $activation, 'License expired', $data ?? []);

            $this->dispatchLicenseExpirationNotification(
                $license,
                $product,
                $variation,
                $activation,
                'license_expired_check'
            );

            return null;
        }

        if (!$activation || $activation->status !== LicenseActivation::STATUS_ACTIVE) {
            $this->log('error', $license, 'validation_failed', $activation, 'No active activation found', $data ?? []);
            return null;
        }

        if (!isset($data['activation_hash'])) {
            $this->log('error', $license, 'validation_failed', $activation, 'Activation hash missing', $data ?? []);
            return null;
        }

        if (!hash_equals($activation->activation_hash, $data['activation_hash'])) {
            $this->log('error', $license, 'validation_failed', $activation, 'Activation hash mismatch', $data ?? []);
            return null;
        }

        $activationExpiryTs = $activation->expire_date ? strtotime((string) $activation->expire_date) : false;
        if ($activation->expire_date && $activationExpiryTs && $activationExpiryTs > 0 && $activationExpiryTs < time()) {
            $this->log('error', $license, 'validation_failed', $activation, 'Activation expired', $data ?? []);

            $this->dispatchLicenseExpirationNotification(
                $license,
                $product,
                $variation,
                $activation,
                'activation_expired_check'
            );

            return null;
        }

        if ($activation->deactivated_at !== null) {
            $this->log('error', $license, 'validation_failed', $activation, 'License is deactivated', $data ?? []);
            return null;
        }

        $activation->last_validated_at = gmdate('Y-m-d H:i:s');
        $activation->save();

        $this->log('success', $license, 'validated', $activation, 'License is valid', [
            'license_status' => $license->status,
            'activation_hash' => $activation->activation_hash,
            'validated_at' => gmdate('Y-m-d H:i:s'),
        ]);

        return $activation;
    }

    /**
     * Validate an activation using an HMAC-signed activation token.
     *
     * Used by the new header-based /validate endpoint.
     * Verifies token signature, fingerprint match, then checks license + activation status.
     *
     * @param string $token      The HMAC-signed activation token from the request header.
     * @param string $fingerprint The site fingerprint from the request header.
     * @return array{valid: bool, activation: ?LicenseActivation, license: ?License, error: ?string, code: ?string}
     */
    public function validateByToken(string $token, string $fingerprint): array
    {
        // Step 1: Verify HMAC signature only (is the token structurally valid?)
        $claims = $this->tokenService->verify($token);
        if ($claims === null) {
            return ['valid' => false, 'activation' => null, 'license' => null, 'error' => 'Invalid or tampered activation token', 'code' => null];
        }

        // Step 2: Verify fingerprint match (is the token being used from the correct site?)
        $sanitizedFingerprint = trim(sanitize_text_field($fingerprint));
        if ($sanitizedFingerprint === '' || !hash_equals($claims['fp'], $sanitizedFingerprint)) {
            return ['valid' => false, 'activation' => null, 'license' => null, 'error' => 'Fingerprint mismatch — activation token is bound to a different site', 'code' => 'fingerprint_mismatch'];
        }

        $activation = LicenseActivation::find($claims['aid']);
        if (!$activation) {
            return ['valid' => false, 'activation' => null, 'license' => null, 'error' => 'Activation not found', 'code' => null];
        }

        // Verify the stored token matches the presented token (guards against invalidated tokens)
        if (!hash_equals($activation->activation_hash, $token)) {
            return ['valid' => false, 'activation' => $activation, 'license' => null, 'error' => 'Activation token revoked or regenerated', 'code' => null];
        }

        if ($activation->status !== LicenseActivation::STATUS_ACTIVE) {
            return ['valid' => false, 'activation' => $activation, 'license' => null, 'error' => 'Activation is not active', 'code' => null];
        }

        if ($activation->deactivated_at !== null) {
            return ['valid' => false, 'activation' => $activation, 'license' => null, 'error' => 'Activation has been deactivated', 'code' => null];
        }

        // Cross-check claim IDs against the activation record
        if (
            (int) $activation->license_id !== $claims['lid'] ||
            (int) $activation->product_id !== $claims['pid'] ||
            (int) $activation->variation_id !== $claims['vid']
        ) {
            return ['valid' => false, 'activation' => $activation, 'license' => null, 'error' => 'Token claims do not match activation record', 'code' => null];
        }

        $license = License::find($claims['lid']);
        if (!$license) {
            return ['valid' => false, 'activation' => $activation, 'license' => null, 'error' => 'License not found', 'code' => null];
        }

        if ($license->status !== License::STATUS_ACTIVE) {
            $this->log('error', $license, 'validation_failed', $activation, 'License is not active');
            return ['valid' => false, 'activation' => $activation, 'license' => $license, 'error' => 'License is not active', 'code' => null];
        }

        if ($license->expire_date && strtotime($license->expire_date) < time()) {
            $product = Product::find((int) $activation->product_id);
            $variation = ProductVariation::find((int) $activation->variation_id);

            $this->log('error', $license, 'validation_failed', $activation, 'License expired');

            $this->dispatchLicenseExpirationNotification(
                $license,
                $product,
                $variation,
                $activation,
                'license_expired_validate'
            );

            return ['valid' => false, 'activation' => $activation, 'license' => $license, 'error' => 'License has expired', 'code' => null];
        }

        // Check activation expiry
        $activationExpiryTs = $activation->expire_date ? strtotime((string) $activation->expire_date) : false;
        if ($activation->expire_date && $activationExpiryTs && $activationExpiryTs > 0 && $activationExpiryTs < time()) {
            $product = Product::find((int) $activation->product_id);
            $variation = ProductVariation::find((int) $activation->variation_id);

            $this->log('error', $license, 'validation_failed', $activation, 'Activation expired');

            $this->dispatchLicenseExpirationNotification(
                $license,
                $product,
                $variation,
                $activation,
                'activation_expired_validate'
            );

            return ['valid' => false, 'activation' => $activation, 'license' => $license, 'error' => 'Activation has expired', 'code' => null];
        }

        // Update last validated timestamp
        $activation->last_validated_at = gmdate('Y-m-d H:i:s');
        $activation->save();

        $this->log('success', $license, 'validated', $activation, 'License validated via activation token');

        return ['valid' => true, 'activation' => $activation, 'license' => $license, 'error' => null, 'code' => null];
    }

    /**
     * Deactivate an activation using an HMAC-signed activation token.
     *
     * Used by the new header-based /deactivate endpoint.
     * The token is permanently invalidated after deactivation.
     *
     * @param string $token      The HMAC-signed activation token from the request header.
     * @param string $fingerprint The site fingerprint from the request header.
     * @return array{success: bool, error: ?string, code: ?string}
     */
    public function deactivateByToken(string $token, string $fingerprint): array
    {
        // Step 1: Verify HMAC signature only
        $claims = $this->tokenService->verify($token);
        if ($claims === null) {
            return ['success' => false, 'error' => 'Invalid or tampered activation token', 'code' => null];
        }

        // Step 2: Verify fingerprint match
        $sanitizedFingerprint = trim(sanitize_text_field($fingerprint));
        if ($sanitizedFingerprint === '' || !hash_equals($claims['fp'], $sanitizedFingerprint)) {
            return ['success' => false, 'error' => 'Fingerprint mismatch — activation token is bound to a different site', 'code' => 'fingerprint_mismatch'];
        }

        $activation = LicenseActivation::find($claims['aid']);
        if (!$activation) {
            return ['success' => false, 'error' => 'Activation not found', 'code' => null];
        }

        // Verify the stored token matches the presented token
        if (!hash_equals($activation->activation_hash, $token)) {
            return ['success' => false, 'error' => 'Activation token revoked or regenerated', 'code' => null];
        }

        if ($activation->status !== LicenseActivation::STATUS_ACTIVE) {
            return ['success' => false, 'error' => 'Activation is not active', 'code' => null];
        }

        // Cross-check claim IDs
        if (
            (int) $activation->license_id !== $claims['lid'] ||
            (int) $activation->product_id !== $claims['pid'] ||
            (int) $activation->variation_id !== $claims['vid']
        ) {
            return ['success' => false, 'error' => 'Token claims do not match activation record', 'code' => null];
        }

        $license = License::find($claims['lid']);
        if (!$license) {
            return ['success' => false, 'error' => 'License not found', 'code' => null];
        }

        DB::beginTransaction();
        try {
            $activation->status = LicenseActivation::STATUS_DEACTIVATED;
            $activation->deactivated_at = gmdate('Y-m-d H:i:s');
            $activation->save();

            $license->activation_count = max(0, $license->activation_count - 1);
            $license->save();

            $licenseProduct = LicenseProduct::where('license_id', $license->id)
                ->where('product_id', $activation->product_id)
                ->where('variation_id', $activation->variation_id)
                ->first();
            if ($licenseProduct) {
                $licenseProduct->activation_count = max(0, $licenseProduct->activation_count - 1);
                $licenseProduct->save();
            }

            $this->log('success', $license, 'deactivated', $activation, 'Activation deactivated via token', [
                'deactivated_at' => $activation->deactivated_at,
            ]);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();
            throw $e;
        }

        return ['success' => true, 'error' => null, 'code' => null];
    }

    public static function transferLicense(int $licenseId, int $newCustomerId, bool $deactivateAll = false): array
    {
        $license = License::find($licenseId);
        if (!$license) {
            return ['success' => false, 'message' => 'License not found'];
        }

        $newCustomer = Customer::find($newCustomerId);
        if (!$newCustomer) {
            return ['success' => false, 'message' => 'Customer not found'];
        }

        $oldCustomerId = (int) $license->customer_id;

        if ($oldCustomerId === $newCustomerId) {
            return ['success' => false, 'message' => 'License already belongs to this customer'];
        }

        DB::beginTransaction();
        try {
            if ($deactivateAll) {
                LicenseActivation::where('license_id', $license->id)
                    ->where('status', LicenseActivation::STATUS_ACTIVE)
                    ->update([
                        'status' => LicenseActivation::STATUS_DEACTIVATED,
                        'deactivated_at' => gmdate('Y-m-d H:i:s'),
                    ]);
                $license->update(['activation_count' => 0]);
            }

            $license->update(['customer_id' => $newCustomerId]);
            $transferredAt = gmdate('Y-m-d H:i:s');

            LicenseEvent::log($license->fresh(), 'transferred', null, null, [
                'response_status' => 'success',
                'response_message' => 'License transferred to new customer',
                'data' => [
                    'old_customer_id' => $oldCustomerId,
                    'new_customer_id' => $newCustomerId,
                    'transferred_by' => get_current_user_id(),
                    'transferred_at' => $transferredAt,
                    'deactivated_all' => $deactivateAll,
                ],
            ]);

            DB::commit();
        } catch (\Throwable $e) {
            DB::rollback();
            throw $e;
        }

        $freshLicense = $license->fresh();
        $freshLicense->load(['customer', 'licenseProducts']);

        return [
            'license' => $freshLicense->toArray(),
            'transfer' => [
                'old_customer_id' => $oldCustomerId,
                'new_customer_id' => $newCustomerId,
                'transferred_by' => get_current_user_id(),
                'transferred_at' => $transferredAt,
                'deactivated_all' => $deactivateAll,
            ],
        ];
    }

    private function log(string $status, License $license, string $event, ?LicenseActivation $activation, string $message, array $data = []): void
    {
        LicenseEvent::log($license, $event, $activation, null, [
            'response_status' => $status,
            'response_message' => $message,
            'response_data' => $data ? wp_json_encode($data) : null,
        ]);
    }

    private function dispatchLicenseActivationNotification(
        License $license,
        Product $product,
        ProductVariation $variation,
        LicenseActivation $activation,
        string $event
    ): void {
        $license->load('customer');

        Action::do('notification_license_activation', [
            'source' => 'license_service',
            'event' => $event,
            'customer' => $this->modelToArray($license->customer),
            'license' => $this->modelToArray($license),
            'product' => $this->modelToArray($product),
            'variation' => $this->modelToArray($variation),
            'activation' => $this->modelToArray($activation),
        ]);
    }

    private function dispatchMaximumActivationsNotification(
        License $license,
        Product $product,
        ProductVariation $variation,
        string $instanceIdentifier
    ): void {
        $license->load('customer');

        Action::do('notification_maximum_activations', [
            'source' => 'license_service',
            'customer' => $this->modelToArray($license->customer),
            'license' => $this->modelToArray($license),
            'product' => $this->modelToArray($product),
            'variation' => $this->modelToArray($variation),
            'meta' => [
                'instance_identifier' => (string) $instanceIdentifier,
                'domain' => (string) $instanceIdentifier,
                'activation_limit' => (int) $license->activation_limit,
            ],
        ]);
    }

    private function dispatchLicenseExpirationNotification(
        License $license,
        ?Product $product,
        ?ProductVariation $variation,
        ?LicenseActivation $activation,
        string $reason
    ): void {
        $license->load('customer');

        $expireDate = $license->expire_date
            ? gmdate('Y-m-d', strtotime((string) $license->expire_date))
            : (
                $activation && !empty($activation->expire_date)
                    ? gmdate('Y-m-d', strtotime((string) $activation->expire_date))
                    : ''
            );

        Action::do('notification_license_expiration', [
            'source' => 'license_service',
            'reason' => $reason,
            'customer' => $this->modelToArray($license->customer),
            'license' => $this->modelToArray($license),
            'product' => $this->modelToArray($product),
            'variation' => $this->modelToArray($variation),
            'activation' => $this->modelToArray($activation),
            'meta' => [
                'domain' => (string) ($activation->instance_identifier ?? ''),
                'expire_date' => $expireDate,
            ],
        ]);
    }

    /**
     * @return array<string,mixed>
     */
    private function modelToArray($model): array
    {
        if (!$model || !method_exists($model, 'toArray')) {
            return [];
        }

        $data = $model->toArray();
        return is_array($data) ? $data : [];
    }
}
