from __future__ import annotations

from datetime import datetime
from threading import Lock
from uuid import UUID

from sqlalchemy.orm import Session

from app.core.security import generate_activation_token, redacted_secret, verify_secret
from app.core.time import as_utc, utc_now
from app.domain.enums import ActivationStatus, DirectServerStatus, LicenceEventType, LicenceStatus, LicenceType
from app.models.entities import Activation, Licence
from app.repositories.licensing import LicensingRepository
from app.schemas.licensing import (
    ActivationRequest,
    ActivationResponse,
    DeactivationRequest,
    EntitlementResponse,
    ValidationRequest,
)
from app.services.errors import (
    ACTIVATION_LIMIT_EXCEEDED,
    EXPIRED_SUBSCRIPTION,
    INVALID_ACTIVATION,
    INVALID_LICENCE,
    REVOKED,
    REVOKED_ACTIVATION,
    SUSPENDED,
    LicensingError,
)


_activation_locks_guard = Lock()
_activation_locks: dict[str, Lock] = {}


def _activation_lock_for(licence_id: UUID) -> Lock:
    key = str(licence_id)
    with _activation_locks_guard:
        lock = _activation_locks.get(key)
        if lock is None:
            lock = Lock()
            _activation_locks[key] = lock
        return lock


class LicensingService:
    def __init__(self, session: Session):
        self.session = session
        self.repository = LicensingRepository(session)

    def activate(self, request: ActivationRequest) -> ActivationResponse:
        licence = self.repository.find_licence_by_plaintext_key(request.licence_key)
        if licence is None:
            self.repository.add_event(
                event_type=LicenceEventType.ACTIVATION_DENIED,
                result="invalid_licence",
                detail={"keyRef": redacted_secret(request.licence_key)},
            )
            self.session.commit()
            raise INVALID_LICENCE

        try:
            with _activation_lock_for(licence.id):
                self._ensure_licence_can_grant_access(licence)
                activation = self.repository.active_activation_for_installation(
                    licence,
                    request.installation_id,
                )
                if activation is None:
                    activation = self.repository.activation_for_installation(
                        licence,
                        request.installation_id,
                    )
                    if activation is not None and activation.status == ActivationStatus.DEACTIVATED:
                        activation.status = ActivationStatus.ACTIVE
                        activation.deactivated_at = None
                        activation.activated_at = utc_now()
                    elif activation is None:
                        if self.repository.count_active_activations(licence) >= licence.activation_limit:
                            self.repository.add_event(
                                event_type=LicenceEventType.ACTIVATION_DENIED,
                                result="activation_limit_exceeded",
                                licence=licence,
                            )
                            self.session.commit()
                            raise ACTIVATION_LIMIT_EXCEEDED
                        activation = Activation(
                            licence=licence,
                            installation_id=request.installation_id,
                            activation_token_identifier="pending",
                            activation_token_suffix="pending",
                            activation_token_hash="pending",
                        )
                        self.repository.add_activation(activation)

                token = self._rotate_activation_token(activation)
                activation.last_validated_at = utc_now()
                self.repository.add_event(
                    event_type=LicenceEventType.ACTIVATION_SUCCEEDED,
                    result="succeeded",
                    licence=licence,
                    activation=activation,
                )
                self.session.commit()
                return self._activation_response(licence, activation, token.plaintext)
        except LicensingError:
            raise

    def validate(self, request: ValidationRequest) -> ActivationResponse:
        activation = self._authenticated_activation(
            request.activation_id,
            request.activation_token,
            request.installation_id,
        )
        licence = activation.licence
        self._ensure_licence_can_grant_access(licence)
        activation.last_validated_at = utc_now()
        self.repository.add_event(
            event_type=LicenceEventType.VALIDATION_SUCCEEDED,
            result="succeeded",
            licence=licence,
            activation=activation,
        )
        self.session.commit()
        return self._activation_response(licence, activation, request.activation_token)

    def deactivate(self, request: DeactivationRequest) -> None:
        activation = self._authenticated_activation(
            request.activation_id,
            request.activation_token,
            request.installation_id,
            allow_deactivated=True,
        )
        if activation.status == ActivationStatus.ACTIVE:
            activation.status = ActivationStatus.DEACTIVATED
            activation.deactivated_at = utc_now()
            self.repository.add_event(
                event_type=LicenceEventType.DEACTIVATION,
                result="succeeded",
                licence=activation.licence,
                activation=activation,
            )
        self.session.commit()

    def _authenticated_activation(
        self,
        activation_id: UUID,
        token: str,
        installation_id: str,
        allow_deactivated: bool = False,
    ) -> Activation:
        activation = self.repository.find_activation_by_id(activation_id)
        if activation is None:
            self.repository.add_event(
                event_type=LicenceEventType.VALIDATION_DENIED,
                result="invalid_activation",
            )
            self.session.commit()
            raise INVALID_ACTIVATION

        if activation.installation_id != installation_id:
            raise INVALID_ACTIVATION
        if not verify_secret(token, activation.activation_token_hash):
            raise INVALID_ACTIVATION
        if activation.status == ActivationStatus.REVOKED:
            raise REVOKED_ACTIVATION
        if activation.status == ActivationStatus.DEACTIVATED and not allow_deactivated:
            raise INVALID_ACTIVATION
        return activation

    def _ensure_licence_can_grant_access(self, licence: Licence) -> None:
        if licence.status == LicenceStatus.SUSPENDED:
            self.repository.add_event(
                event_type=LicenceEventType.VALIDATION_DENIED,
                result="suspended",
                licence=licence,
            )
            self.session.commit()
            raise SUSPENDED
        if licence.status == LicenceStatus.REVOKED:
            self.repository.add_event(
                event_type=LicenceEventType.VALIDATION_DENIED,
                result="revoked",
                licence=licence,
            )
            self.session.commit()
            raise REVOKED
        if licence.licence_type == LicenceType.SUBSCRIPTION and self._subscription_expired(licence):
            self.repository.add_event(
                event_type=LicenceEventType.VALIDATION_DENIED,
                result="expired_subscription",
                licence=licence,
            )
            self.session.commit()
            raise EXPIRED_SUBSCRIPTION

    def _subscription_expired(self, licence: Licence) -> bool:
        return licence.subscription_expiry is not None and as_utc(licence.subscription_expiry) <= utc_now()

    def _rotate_activation_token(self, activation: Activation):
        token = generate_activation_token()
        activation.activation_token_identifier = token.public_identifier
        activation.activation_token_suffix = token.suffix
        activation.activation_token_hash = token.hashed
        activation.activation_token_hash_version = 1
        return token

    def _activation_response(
        self,
        licence: Licence,
        activation: Activation,
        plaintext_token: str,
    ) -> ActivationResponse:
        return ActivationResponse(
            activation_id=activation.id,
            activation_token=plaintext_token,
            entitlements=self._entitlements_for_licence(licence),
        )

    def _entitlements_for_licence(self, licence: Licence) -> EntitlementResponse:
        server_time = utc_now()
        status = self._server_status_for_licence(licence, server_time)
        application_access = status == DirectServerStatus.ACTIVE
        licence_expiry = (
            as_utc(licence.subscription_expiry)
            if licence.licence_type == LicenceType.SUBSCRIPTION
            and licence.subscription_expiry is not None
            else None
        )
        update_entitlement_expiry = (
            as_utc(licence.update_entitlement_expiry)
            if licence.update_entitlement_expiry is not None
            else None
        )
        support_entitlement_expiry = (
            as_utc(licence.support_entitlement_expiry)
            if licence.support_entitlement_expiry is not None
            else None
        )

        return EntitlementResponse(
            status=status,
            licence_type=licence.licence_type,
            application_access=application_access,
            updates_allowed=licence.update_entitlement_expiry is not None or application_access,
            support_allowed=licence.support_entitlement_expiry is not None or application_access,
            subscription_expiry_date=licence_expiry,
            update_entitlement_expiry_date=update_entitlement_expiry,
            support_entitlement_expiry_date=support_entitlement_expiry,
            server_time=server_time,
            grace_period_ends_at=None,
            offline_valid_until=None,
        )

    def _server_status_for_licence(
        self,
        licence: Licence,
        server_time: datetime,
    ) -> DirectServerStatus:
        if licence.status == LicenceStatus.SUSPENDED:
            return DirectServerStatus.SUSPENDED
        if licence.status == LicenceStatus.REVOKED:
            return DirectServerStatus.REVOKED
        if licence.licence_type == LicenceType.SUBSCRIPTION and licence.subscription_expiry is not None:
            if as_utc(licence.subscription_expiry) <= server_time:
                return DirectServerStatus.EXPIRED_SUBSCRIPTION
        if licence.status == LicenceStatus.EXPIRED:
            return DirectServerStatus.EXPIRED_SUBSCRIPTION
        return DirectServerStatus.ACTIVE
