import logging
import uuid
from collections.abc import Mapping
from contextlib import contextmanager
from datetime import datetime
from typing import Any

from flask import Request, Response
from pydantic import TypeAdapter

from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.entities.request import TriggerDispatchResponse
from core.tools.errors import ToolProviderCredentialValidationError
from core.trigger.entities.api_entities import SubscriptionBuilderApiEntity
from core.trigger.entities.entities import (
    RequestLog,
    Subscription,
    SubscriptionBuilder,
    SubscriptionBuilderUpdater,
    SubscriptionConstructor,
)
from core.trigger.provider import PluginTriggerProviderController
from core.trigger.trigger_manager import TriggerManager
from core.trigger.utils.encryption import masked_credentials
from core.trigger.utils.endpoint import generate_plugin_trigger_endpoint_url
from extensions.ext_redis import redis_client
from models.provider_ids import TriggerProviderID
from services.trigger.trigger_provider_service import TriggerProviderService

logger = logging.getLogger(__name__)

_request_logs_adapter: TypeAdapter[list[RequestLog]] = TypeAdapter(list[RequestLog])


class TriggerSubscriptionBuilderService:
    """Service for managing trigger providers and credentials"""

    ##########################
    # Trigger provider
    ##########################
    __MAX_TRIGGER_PROVIDER_COUNT__ = 10

    ##########################
    # Builder endpoint
    ##########################
    __BUILDER_CACHE_EXPIRE_SECONDS__ = 30 * 60

    __VALIDATION_REQUEST_CACHE_COUNT__ = 10
    __VALIDATION_REQUEST_CACHE_EXPIRE_SECONDS__ = 30 * 60

    ##########################
    # Distributed lock
    ##########################
    __LOCK_EXPIRE_SECONDS__ = 30

    @classmethod
    def encode_cache_key(cls, subscription_id: str) -> str:
        return f"trigger:subscription:builder:{subscription_id}"

    @classmethod
    def encode_lock_key(cls, subscription_id: str) -> str:
        return f"trigger:subscription:builder:lock:{subscription_id}"

    @classmethod
    @contextmanager
    def acquire_builder_lock(cls, subscription_id: str):
        """
        Acquire a distributed lock for a subscription builder.

        :param subscription_id: The subscription builder ID
        """
        lock_key = cls.encode_lock_key(subscription_id)
        with redis_client.lock(lock_key, timeout=cls.__LOCK_EXPIRE_SECONDS__):
            yield

    @classmethod
    def verify_trigger_subscription_builder(
        cls,
        tenant_id: str,
        user_id: str,
        provider_id: TriggerProviderID,
        subscription_builder_id: str,
    ) -> Mapping[str, Any]:
        """Verify a trigger subscription builder"""
        provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
        if not provider_controller:
            raise ValueError(f"Provider {provider_id} not found")

        subscription_builder = cls.get_subscription_builder(subscription_builder_id)
        if not subscription_builder:
            raise ValueError(f"Subscription builder {subscription_builder_id} not found")

        if subscription_builder.credential_type == CredentialType.OAUTH2:
            return {"verified": bool(subscription_builder.credentials)}

        if subscription_builder.credential_type == CredentialType.API_KEY:
            credentials_to_validate = subscription_builder.credentials
            try:
                provider_controller.validate_credentials(user_id, credentials_to_validate)
            except ToolProviderCredentialValidationError as e:
                raise ValueError(f"Invalid credentials: {e}")
            return {"verified": True}

        return {"verified": True}

    @classmethod
    def build_trigger_subscription_builder(
        cls, tenant_id: str, user_id: str, provider_id: TriggerProviderID, subscription_builder_id: str
    ) -> None:
        """Build a trigger subscription builder"""
        provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
        if not provider_controller:
            raise ValueError(f"Provider {provider_id} not found")

        # Acquire lock to prevent concurrent build operations
        with cls.acquire_builder_lock(subscription_builder_id):
            subscription_builder = cls.get_subscription_builder(subscription_builder_id)
            if not subscription_builder:
                raise ValueError(f"Subscription builder {subscription_builder_id} not found")

            if not subscription_builder.name:
                raise ValueError("Subscription builder name is required")

            credential_type = CredentialType.of(subscription_builder.credential_type or CredentialType.UNAUTHORIZED)
            if credential_type == CredentialType.UNAUTHORIZED:
                # manually create
                TriggerProviderService.add_trigger_subscription(
                    subscription_id=subscription_builder.id,
                    tenant_id=tenant_id,
                    user_id=user_id,
                    name=subscription_builder.name,
                    provider_id=provider_id,
                    endpoint_id=subscription_builder.endpoint_id,
                    parameters=subscription_builder.parameters,
                    properties=subscription_builder.properties,
                    credential_expires_at=subscription_builder.credential_expires_at or -1,
                    expires_at=subscription_builder.expires_at,
                    credentials=subscription_builder.credentials,
                    credential_type=credential_type,
                )
            else:
                # automatically create
                subscription: Subscription = TriggerManager.subscribe_trigger(
                    tenant_id=tenant_id,
                    user_id=user_id,
                    provider_id=provider_id,
                    endpoint=generate_plugin_trigger_endpoint_url(subscription_builder.endpoint_id),
                    parameters=subscription_builder.parameters,
                    credentials=subscription_builder.credentials,
                    credential_type=credential_type,
                )

                TriggerProviderService.add_trigger_subscription(
                    subscription_id=subscription_builder.id,
                    tenant_id=tenant_id,
                    user_id=user_id,
                    name=subscription_builder.name,
                    provider_id=provider_id,
                    endpoint_id=subscription_builder.endpoint_id,
                    parameters=subscription_builder.parameters,
                    properties=subscription.properties,
                    credentials=subscription_builder.credentials,
                    credential_type=credential_type,
                    credential_expires_at=subscription_builder.credential_expires_at or -1,
                    expires_at=subscription_builder.expires_at,
                )

            # Delete the builder after successful subscription creation
            cache_key = cls.encode_cache_key(subscription_builder_id)
            redis_client.delete(cache_key)

    @classmethod
    def create_trigger_subscription_builder(
        cls,
        tenant_id: str,
        user_id: str,
        provider_id: TriggerProviderID,
        credential_type: CredentialType,
    ) -> SubscriptionBuilderApiEntity:
        """
        Add a new trigger subscription validation.
        """
        provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
        if not provider_controller:
            raise ValueError(f"Provider {provider_id} not found")

        subscription_constructor: SubscriptionConstructor | None = provider_controller.get_subscription_constructor()
        subscription_id = str(uuid.uuid4())
        subscription_builder = SubscriptionBuilder(
            id=subscription_id,
            name=None,
            endpoint_id=subscription_id,
            tenant_id=tenant_id,
            user_id=user_id,
            provider_id=str(provider_id),
            parameters=subscription_constructor.get_default_parameters() if subscription_constructor else {},
            properties=provider_controller.get_subscription_default_properties(),
            credentials={},
            credential_type=credential_type,
            credential_expires_at=-1,
            expires_at=-1,
        )
        cache_key = cls.encode_cache_key(subscription_id)
        redis_client.setex(cache_key, cls.__BUILDER_CACHE_EXPIRE_SECONDS__, subscription_builder.model_dump_json())
        return cls.builder_to_api_entity(controller=provider_controller, entity=subscription_builder)

    @classmethod
    def update_trigger_subscription_builder(
        cls,
        tenant_id: str,
        provider_id: TriggerProviderID,
        subscription_builder_id: str,
        subscription_builder_updater: SubscriptionBuilderUpdater,
    ) -> SubscriptionBuilderApiEntity:
        """
        Update a trigger subscription validation.
        """
        subscription_id = subscription_builder_id
        provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
        if not provider_controller:
            raise ValueError(f"Provider {provider_id} not found")

        # Acquire lock to prevent concurrent updates
        with cls.acquire_builder_lock(subscription_id):
            cache_key = cls.encode_cache_key(subscription_id)
            subscription_builder_cache = cls.get_subscription_builder(subscription_builder_id)
            if not subscription_builder_cache or subscription_builder_cache.tenant_id != tenant_id:
                raise ValueError(f"Subscription {subscription_id} expired or not found")

            subscription_builder_updater.update(subscription_builder_cache)

            redis_client.setex(
                cache_key, cls.__BUILDER_CACHE_EXPIRE_SECONDS__, subscription_builder_cache.model_dump_json()
            )
            return cls.builder_to_api_entity(controller=provider_controller, entity=subscription_builder_cache)

    @classmethod
    def update_and_verify_builder(
        cls,
        tenant_id: str,
        user_id: str,
        provider_id: TriggerProviderID,
        subscription_builder_id: str,
        subscription_builder_updater: SubscriptionBuilderUpdater,
    ) -> Mapping[str, Any]:
        """
        Atomically update and verify a subscription builder.
        This ensures the verification is done on the exact data that was just updated.
        """
        subscription_id = subscription_builder_id
        provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
        if not provider_controller:
            raise ValueError(f"Provider {provider_id} not found")

        # Acquire lock for the entire update + verify operation
        with cls.acquire_builder_lock(subscription_id):
            cache_key = cls.encode_cache_key(subscription_id)
            subscription_builder_cache = cls.get_subscription_builder(subscription_builder_id)
            if not subscription_builder_cache or subscription_builder_cache.tenant_id != tenant_id:
                raise ValueError(f"Subscription {subscription_id} expired or not found")

            # Update
            subscription_builder_updater.update(subscription_builder_cache)
            redis_client.setex(
                cache_key, cls.__BUILDER_CACHE_EXPIRE_SECONDS__, subscription_builder_cache.model_dump_json()
            )

            # Verify (using the just-updated data)
            if subscription_builder_cache.credential_type == CredentialType.OAUTH2:
                return {"verified": bool(subscription_builder_cache.credentials)}

            if subscription_builder_cache.credential_type == CredentialType.API_KEY:
                credentials_to_validate = subscription_builder_cache.credentials
                try:
                    provider_controller.validate_credentials(user_id, credentials_to_validate)
                except ToolProviderCredentialValidationError as e:
                    raise ValueError(f"Invalid credentials: {e}")
                return {"verified": True}

            return {"verified": True}

    @classmethod
    def update_and_build_builder(
        cls,
        tenant_id: str,
        user_id: str,
        provider_id: TriggerProviderID,
        subscription_builder_id: str,
        subscription_builder_updater: SubscriptionBuilderUpdater,
    ) -> None:
        """
        Atomically update and build a subscription builder.
        This ensures the build uses the exact data that was just updated.
        """
        subscription_id = subscription_builder_id
        provider_controller = TriggerManager.get_trigger_provider(tenant_id, provider_id)
        if not provider_controller:
            raise ValueError(f"Provider {provider_id} not found")

        # Acquire lock for the entire update + build operation
        with cls.acquire_builder_lock(subscription_id):
            cache_key = cls.encode_cache_key(subscription_id)
            subscription_builder_cache = cls.get_subscription_builder(subscription_builder_id)
            if not subscription_builder_cache or subscription_builder_cache.tenant_id != tenant_id:
                raise ValueError(f"Subscription {subscription_id} expired or not found")

            # Update
            subscription_builder_updater.update(subscription_builder_cache)
            redis_client.setex(
                cache_key, cls.__BUILDER_CACHE_EXPIRE_SECONDS__, subscription_builder_cache.model_dump_json()
            )

            # Re-fetch to ensure we have the latest data
            subscription_builder = cls.get_subscription_builder(subscription_builder_id)
            if not subscription_builder:
                raise ValueError(f"Subscription builder {subscription_builder_id} not found")

            if not subscription_builder.name:
                raise ValueError("Subscription builder name is required")

            # Build
            credential_type = CredentialType.of(subscription_builder.credential_type or CredentialType.UNAUTHORIZED)
            if credential_type == CredentialType.UNAUTHORIZED:
                # manually create
                TriggerProviderService.add_trigger_subscription(
                    subscription_id=subscription_builder.id,
                    tenant_id=tenant_id,
                    user_id=user_id,
                    name=subscription_builder.name,
                    provider_id=provider_id,
                    endpoint_id=subscription_builder.endpoint_id,
                    parameters=subscription_builder.parameters,
                    properties=subscription_builder.properties,
                    credential_expires_at=subscription_builder.credential_expires_at or -1,
                    expires_at=subscription_builder.expires_at,
                    credentials=subscription_builder.credentials,
                    credential_type=credential_type,
                )
            else:
                # automatically create
                subscription: Subscription = TriggerManager.subscribe_trigger(
                    tenant_id=tenant_id,
                    user_id=user_id,
                    provider_id=provider_id,
                    endpoint=generate_plugin_trigger_endpoint_url(subscription_builder.endpoint_id),
                    parameters=subscription_builder.parameters,
                    credentials=subscription_builder.credentials,
                    credential_type=credential_type,
                )

                TriggerProviderService.add_trigger_subscription(
                    subscription_id=subscription_builder.id,
                    tenant_id=tenant_id,
                    user_id=user_id,
                    name=subscription_builder.name,
                    provider_id=provider_id,
                    endpoint_id=subscription_builder.endpoint_id,
                    parameters=subscription_builder.parameters,
                    properties=subscription.properties,
                    credentials=subscription_builder.credentials,
                    credential_type=credential_type,
                    credential_expires_at=subscription_builder.credential_expires_at or -1,
                    expires_at=subscription_builder.expires_at,
                )

            # Delete the builder after successful subscription creation
            cache_key = cls.encode_cache_key(subscription_builder_id)
            redis_client.delete(cache_key)

    @classmethod
    def builder_to_api_entity(
        cls, controller: PluginTriggerProviderController, entity: SubscriptionBuilder
    ) -> SubscriptionBuilderApiEntity:
        credential_type = CredentialType.of(entity.credential_type or CredentialType.UNAUTHORIZED.value)
        return SubscriptionBuilderApiEntity(
            id=entity.id,
            name=entity.name or "",
            provider=entity.provider_id,
            endpoint=generate_plugin_trigger_endpoint_url(entity.endpoint_id),
            parameters=entity.parameters,
            properties=entity.properties,
            credential_type=credential_type,
            credentials=masked_credentials(
                schemas=controller.get_credentials_schema(credential_type),
                credentials=entity.credentials,
            )
            if controller.get_subscription_constructor()
            else {},
        )

    @classmethod
    def get_subscription_builder(cls, endpoint_id: str) -> SubscriptionBuilder | None:
        """
        Get a trigger subscription by the endpoint ID.
        """
        cache_key = cls.encode_cache_key(endpoint_id)
        subscription_cache = redis_client.get(cache_key)
        if subscription_cache:
            return SubscriptionBuilder.model_validate_json(subscription_cache)

        return None

    @classmethod
    def append_log(cls, endpoint_id: str, request: Request, response: Response) -> None:
        """Append validation request log to Redis."""
        log = RequestLog(
            id=str(uuid.uuid4()),
            endpoint=endpoint_id,
            request={
                "method": request.method,
                "url": request.url,
                "headers": dict(request.headers),
                "data": request.get_data(as_text=True),
            },
            response={
                "status_code": response.status_code,
                "headers": dict(response.headers),
                "data": response.get_data(as_text=True),
            },
            created_at=datetime.now(),
        )

        key = f"trigger:subscription:builder:logs:{endpoint_id}"
        logs = _request_logs_adapter.validate_json(redis_client.get(key) or b"[]")
        logs.append(log)

        # Keep last N logs
        logs = logs[-cls.__VALIDATION_REQUEST_CACHE_COUNT__ :]
        redis_client.setex(
            key,
            cls.__VALIDATION_REQUEST_CACHE_EXPIRE_SECONDS__,
            _request_logs_adapter.dump_json(logs),
        )

    @classmethod
    def list_logs(cls, endpoint_id: str) -> list[RequestLog]:
        """List request logs for validation endpoint."""
        key = f"trigger:subscription:builder:logs:{endpoint_id}"
        logs_json = redis_client.get(key)
        if not logs_json:
            return []
        return _request_logs_adapter.validate_json(logs_json)

    @classmethod
    def process_builder_validation_endpoint(cls, endpoint_id: str, request: Request) -> Response | None:
        """
        Process a temporary endpoint request.

        :param endpoint_id: The endpoint identifier
        :param request: The Flask request object
        :return: The Flask response object
        """
        # check if validation endpoint exists
        subscription_builder: SubscriptionBuilder | None = cls.get_subscription_builder(endpoint_id)
        if not subscription_builder:
            return None

        try:
            # response to validation endpoint
            controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider(
                tenant_id=subscription_builder.tenant_id,
                provider_id=TriggerProviderID(subscription_builder.provider_id),
            )
            dispatch_response: TriggerDispatchResponse = controller.dispatch(
                request=request,
                subscription=subscription_builder.to_subscription(),
                credentials={},
                credential_type=CredentialType.UNAUTHORIZED,
            )
            response: Response = dispatch_response.response
            # append the request log
            cls.append_log(
                endpoint_id=endpoint_id,
                request=request,
                response=response,
            )
            return response
        except Exception:
            logger.exception("Error during validation endpoint dispatch for endpoint_id=%s", endpoint_id)
            error_response = Response(status=500, response="An internal error has occurred.")
            cls.append_log(endpoint_id=endpoint_id, request=request, response=error_response)
            return error_response

    @classmethod
    def get_subscription_builder_by_id(cls, subscription_builder_id: str) -> SubscriptionBuilderApiEntity:
        """Get a trigger subscription builder API entity."""
        subscription_builder = cls.get_subscription_builder(subscription_builder_id)
        if not subscription_builder:
            raise ValueError(f"Subscription builder {subscription_builder_id} not found")
        return cls.builder_to_api_entity(
            controller=TriggerManager.get_trigger_provider(
                subscription_builder.tenant_id, TriggerProviderID(subscription_builder.provider_id)
            ),
            entity=subscription_builder,
        )
