"""OTP platform — generation, verification, expiry, rate limit, brute-force protection.

IMPORTANT: This OTP module is for **business/platform messaging OTP** only.
Core Platform auth OTP (`POST /api/v1/auth/otp/*` via Payamak in core-service)
remains independent and must NOT be replaced or coupled here (ADR-012).
Reserved Core purposes such as `auth.login` / `auth.verify_mobile` must not be
used as Communication challenge purposes for platform messaging flows.
"""
from __future__ import annotations

import hashlib
import secrets
from datetime import datetime, timedelta, timezone
from typing import Any
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.events.publisher import get_event_publisher
from app.events.types import CommunicationEventType
from app.models.foundation import OTPChallenge
from app.models.types import OTPStatus
from app.repositories.foundation import OTPChallengeRepository
from app.services.message_service import MessageService
from app.validators import validate_otp_request, validate_otp_verify
from shared.exceptions import AppError, NotFoundError

_RESERVED_CORE_PURPOSES = frozenset(
    {"auth.login", "auth.verify_mobile", "core.otp", "identity.otp"}
)


def _hash_code(code: str, tenant_id: UUID, destination: str) -> str:
    raw = f"{tenant_id}:{destination}:{code}"
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


class OTPService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.repo = OTPChallengeRepository(session)
        self.messages = MessageService(session)
        self.events = get_event_publisher()

    async def request(
        self, tenant_id: UUID, data: dict[str, Any], *, actor_id: str | None = None
    ) -> tuple[OTPChallenge, str | None]:
        validate_otp_request(data)
        purpose = data.get("purpose")
        if purpose in _RESERVED_CORE_PURPOSES:
            raise AppError(
                "Purpose reserved for Core auth OTP — use Core /auth/otp endpoints",
                status_code=422,
                error_code="otp_purpose_reserved",
            )
        destination = data["destination"]
        channel = data.get("channel", "sms")
        since = datetime.now(timezone.utc) - timedelta(minutes=1)
        recent = await self.repo.count_recent(tenant_id, destination, since)
        if recent >= settings.otp_rate_limit_per_minute:
            raise AppError(
                "OTP rate limit exceeded",
                status_code=429,
                error_code="otp_rate_limited",
            )

        length = int(data.get("code_length") or 6)
        length = min(8, max(4, length))
        code = "".join(secrets.choice("0123456789") for _ in range(length))
        ttl = int(data.get("ttl_seconds") or settings.otp_default_ttl_seconds)
        expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl)

        challenge = OTPChallenge(
            tenant_id=tenant_id,
            channel=channel,
            destination=destination,
            code_hash=_hash_code(code, tenant_id, destination),
            status=OTPStatus.PENDING.value,
            max_attempts=settings.otp_max_attempts,
            expires_at=expires_at,
            purpose=purpose,
            metadata_json={"code_length": length},
        )
        await self.repo.add(challenge)

        sent = await self.messages.send(
            tenant_id,
            {
                "channel": channel,
                "to_address": destination,
                "body": f"کد تایید شما: {code}",
                "priority": "high",
                "source_module": "communication.otp",
                "correlation_id": f"otp:{challenge.id}",
                "process_immediately": True,
            },
            actor_id=actor_id,
        )
        if sent:
            challenge.message_id = sent[0].id
        await self.session.commit()
        await self.session.refresh(challenge)

        self.events.publish(
            event_type=CommunicationEventType.OTP_GENERATED,
            aggregate_type="otp_challenge",
            aggregate_id=challenge.id,
            tenant_id=tenant_id,
            payload={"channel": channel, "destination": destination, "purpose": purpose},
        )

        debug_code = code if settings.environment in ("development", "test") else None
        return challenge, debug_code

    async def verify(self, tenant_id: UUID, data: dict[str, Any]) -> dict[str, Any]:
        validate_otp_verify(data)
        destination = data["destination"]
        code = str(data["code"])
        challenge: OTPChallenge | None = None
        if data.get("challenge_id"):
            challenge = await self.repo.get(tenant_id, data["challenge_id"])
        else:
            challenge = await self.repo.get_latest_pending(tenant_id, destination)

        if challenge is None:
            raise NotFoundError("OTP challenge not found")

        now = datetime.now(timezone.utc)
        expires_at = challenge.expires_at
        if expires_at.tzinfo is None:
            expires_at = expires_at.replace(tzinfo=timezone.utc)
        if challenge.status == OTPStatus.LOCKED.value:
            raise AppError(
                "OTP locked due to too many attempts",
                status_code=423,
                error_code="otp_locked",
            )
        if challenge.status != OTPStatus.PENDING.value:
            raise AppError(
                f"OTP challenge is {challenge.status}",
                status_code=422,
                error_code="otp_invalid_status",
            )
        if expires_at <= now:
            challenge.status = OTPStatus.EXPIRED.value
            await self.session.commit()
            raise AppError("OTP expired", status_code=410, error_code="otp_expired")

        challenge.attempts += 1
        expected = _hash_code(code, tenant_id, destination)
        if challenge.code_hash != expected:
            if challenge.attempts >= challenge.max_attempts:
                challenge.status = OTPStatus.LOCKED.value
            await self.session.commit()
            self.events.publish(
                event_type=CommunicationEventType.OTP_FAILED,
                aggregate_type="otp_challenge",
                aggregate_id=challenge.id,
                tenant_id=tenant_id,
                payload={"attempts": challenge.attempts, "destination": destination},
            )
            raise AppError(
                "Invalid OTP code",
                status_code=400,
                error_code="otp_invalid",
                details={"attempts": challenge.attempts},
            )

        challenge.status = OTPStatus.VERIFIED.value
        challenge.verified_at = now
        await self.session.commit()
        self.events.publish(
            event_type=CommunicationEventType.OTP_VERIFIED,
            aggregate_type="otp_challenge",
            aggregate_id=challenge.id,
            tenant_id=tenant_id,
            payload={"destination": destination, "purpose": challenge.purpose},
        )
        return {
            "verified": True,
            "challenge_id": challenge.id,
            "status": challenge.status,
        }
