"""Devices domain - business logic (pairing lifecycle, heartbeat, revocation)."""
from __future__ import annotations
import secrets
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
from uuid import UUID

from src.core.exceptions import ForbiddenError, NotFoundError, ValidationError
from src.core.security.hashing import hash_token
from src.domain.devices.models import Device, DeviceStatus
from src.domain.devices.repository import DeviceEventRepository, DeviceRepository

PAIRING_TTL_MINUTES = 15
_USER_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"


def _generate_device_code() -> str:
    return "dc_" + secrets.token_urlsafe(32)


def _generate_user_code() -> str:
    return "".join(secrets.choice(_USER_CODE_ALPHABET) for _ in range(8))


def _generate_device_secret() -> str:
    return "dvc_" + secrets.token_urlsafe(32)


class DeviceService:
    def __init__(
        self,
        *,
        devices: DeviceRepository,
        events: DeviceEventRepository,
        app_url: str,
    ) -> None:
        self._devices = devices
        self._events = events
        self._app_url = app_url.rstrip("/")

    async def start_pairing(
        self,
        *,
        name: Optional[str] = None,
        hostname: Optional[str] = None,
        os: Optional[str] = None,
        os_version: Optional[str] = None,
        app_version: Optional[str] = None,
    ) -> dict[str, Any]:
        device_code = _generate_device_code()
        user_code = _generate_user_code()
        expires_at = datetime.now(timezone.utc) + timedelta(minutes=PAIRING_TTL_MINUTES)
        device = await self._devices.create_pending(
            device_code=device_code,
            user_code=user_code,
            pairing_expires_at=expires_at,
            name=name,
            hostname=hostname,
            os=os,
            os_version=os_version,
            app_version=app_version,
        )
        return {
            "device_id": device.id,
            "device_code": device_code,
            "user_code": user_code,
            "verification_uri": f"{self._app_url}/pair?code={user_code}",
            "expires_at": expires_at,
        }

    async def poll_pairing(self, device_code: str) -> dict[str, Any]:
        device = await self._devices.get_by_device_code(device_code)
        if not device:
            return {"status": "already_paired"}

        if device.status == DeviceStatus.revoked.value:
            return {"status": "revoked"}

        if device.status == DeviceStatus.pending_pairing.value:
            if device.pairing_expires_at and device.pairing_expires_at < datetime.now(timezone.utc):
                return {"status": "expired"}
            return {"status": "pending"}

        if device.status == DeviceStatus.active.value:
            if device.device_secret_hash:
                return {"status": "already_paired", "device_id": device.id, "user_id": device.user_id}

            raw_secret = _generate_device_secret()
            delivered = await self._devices.deliver_secret_if_missing(
                device.id,
                hash_token(raw_secret),
            )
            if not delivered:
                refreshed = await self._devices.get_by_id(device.id)
                if refreshed and refreshed.device_secret_hash:
                    return {
                        "status": "already_paired",
                        "device_id": refreshed.id,
                        "user_id": refreshed.user_id,
                    }
                return {"status": "pending"}
            return {
                "status": "approved",
                "device_id": device.id,
                "user_id": device.user_id,
                "device_secret": raw_secret,
            }

        return {"status": "pending"}

    async def get_pending_by_user_code(self, user_code: str) -> Device:
        code = user_code.strip().upper()
        device = await self._devices.get_by_user_code(code)
        if not device:
            raise NotFoundError("Pairing code not found")
        if device.status != DeviceStatus.pending_pairing.value:
            raise ValidationError(f"Device is not awaiting approval (status: {device.status})")
        if device.pairing_expires_at and device.pairing_expires_at < datetime.now(timezone.utc):
            raise ValidationError("Pairing code has expired; start pairing again")
        return device

    async def approve_pairing(self, *, user_code: str, user_id: UUID) -> Device:
        device = await self.get_pending_by_user_code(user_code)
        approved = await self._devices.approve_if_pending(device.id, user_id=user_id)
        if not approved:
            refreshed = await self._devices.get_by_id(device.id)
            if not refreshed:
                raise NotFoundError("Device not found")
            if refreshed.status == DeviceStatus.active.value and refreshed.user_id == user_id:
                return refreshed
            if refreshed.status == DeviceStatus.active.value and refreshed.user_id != user_id:
                raise ForbiddenError("This device was already paired to another account")
            raise ValidationError(f"Device is not awaiting approval (status: {refreshed.status})")
        device = await self._devices.get_by_id(device.id)
        if not device:
            raise NotFoundError("Device not found")
        await self._events.create(
            device.id, event_type="paired", triggered_by="user",
            metadata={"user_id": str(user_id)},
        )
        return device

    async def heartbeat(
        self,
        device: Device,
        *,
        capabilities: Optional[dict[str, Any]] = None,
        last_seen_ip: Optional[str] = None,
    ) -> Device:
        if device.status == DeviceStatus.revoked.value:
            raise ForbiddenError("Device has been revoked")
        was_offline = device.status in (DeviceStatus.offline.value, DeviceStatus.unhealthy.value)
        device = await self._devices.update_heartbeat(
            device, capabilities=capabilities, last_seen_ip=last_seen_ip
        )
        if was_offline:
            await self._events.create(device.id, event_type="recovered", triggered_by="device")
        return device

    async def list_for_user(self, user_id: UUID) -> list[Device]:
        return await self._devices.list_for_user(user_id)

    async def list_recent_for_workspace(self, workspace_id: UUID) -> list[Device]:
        return await self._devices.list_recent_for_workspace(workspace_id)

    async def revoke_own_device(self, user_id: UUID, device_id: UUID) -> Device:
        device = await self._devices.get_by_id(device_id)
        if not device or device.user_id != user_id:
            raise NotFoundError("Device not found")
        device = await self._devices.revoke(device, revoked_by=user_id)
        await self._events.create(
            device.id, event_type="revoked", triggered_by="user",
            metadata={"revoked_by": str(user_id)},
        )
        return device
