"""Devices domain - data access layer."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any, Optional
from uuid import UUID, uuid4

from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession

from src.domain.devices.models import Device, DeviceEvent, DeviceStatus


class DeviceRepository:
    def __init__(self, db: AsyncSession):
        self._db = db

    async def get_by_id(self, device_id: UUID) -> Optional[Device]:
        result = await self._db.execute(select(Device).where(Device.id == device_id))
        return result.scalar_one_or_none()

    async def get_by_user_code(self, user_code: str) -> Optional[Device]:
        result = await self._db.execute(select(Device).where(Device.user_code == user_code))
        return result.scalar_one_or_none()

    async def get_by_device_code(self, device_code: str) -> Optional[Device]:
        result = await self._db.execute(select(Device).where(Device.device_code == device_code))
        return result.scalar_one_or_none()

    async def set_last_workspace(self, device: Device, workspace_id: Optional[UUID]) -> Device:
        device.last_workspace_id = workspace_id
        await self._db.flush()
        return device

    async def create_pending(
        self,
        *,
        device_code: str,
        user_code: str,
        pairing_expires_at: datetime,
        name: Optional[str] = None,
        hostname: Optional[str] = None,
        os: Optional[str] = None,
        os_version: Optional[str] = None,
        app_version: Optional[str] = None,
    ) -> Device:
        device = Device(
            id=uuid4(),
            status=DeviceStatus.pending_pairing.value,
            device_code=device_code,
            user_code=user_code,
            pairing_expires_at=pairing_expires_at,
            name=name,
            hostname=hostname,
            os=os,
            os_version=os_version,
            app_version=app_version,
        )
        self._db.add(device)
        await self._db.flush()
        await self._db.refresh(device)
        return device

    async def approve(self, device: Device, *, user_id: UUID) -> Device:
        device.user_id = user_id
        device.status = DeviceStatus.active.value
        device.paired_at = datetime.now(timezone.utc)
        await self._db.flush()
        return device

    async def approve_if_pending(self, device_id: UUID, *, user_id: UUID) -> bool:
        result = await self._db.execute(
            update(Device)
            .where(Device.id == device_id)
            .where(Device.status == DeviceStatus.pending_pairing.value)
            .values(
                user_id=user_id,
                status=DeviceStatus.active.value,
                paired_at=datetime.now(timezone.utc),
            )
        )
        await self._db.flush()
        return result.rowcount == 1

    async def deliver_secret(self, device: Device, device_secret_hash: str) -> Device:
        """Store the hashed secret and clear pairing fields — the raw secret is shown exactly once
        to the caller of the poll endpoint that triggers this; it is never persisted or replayed."""
        device.device_secret_hash = device_secret_hash
        device.device_code = None
        device.user_code = None
        device.pairing_expires_at = None
        await self._db.flush()
        return device

    async def deliver_secret_if_missing(self, device_id: UUID, device_secret_hash: str) -> bool:
        """Atomically install the durable device secret exactly once."""
        result = await self._db.execute(
            update(Device)
            .where(Device.id == device_id)
            .where(Device.status == DeviceStatus.active.value)
            .where(Device.device_secret_hash.is_(None))
            .values(
                device_secret_hash=device_secret_hash,
                device_code=None,
                user_code=None,
                pairing_expires_at=None,
            )
        )
        await self._db.flush()
        return result.rowcount == 1

    async def update_heartbeat(
        self,
        device: Device,
        *,
        capabilities: Optional[dict[str, Any]] = None,
        last_seen_ip: Optional[str] = None,
    ) -> Device:
        device.last_heartbeat_at = datetime.now(timezone.utc)
        if capabilities is not None:
            device.capabilities = capabilities
        if last_seen_ip is not None:
            device.last_seen_ip = last_seen_ip
        # A device that heartbeats again after being flagged offline/unhealthy recovers.
        if device.status in (DeviceStatus.offline.value, DeviceStatus.unhealthy.value):
            device.status = DeviceStatus.active.value
        await self._db.flush()
        return device

    async def revoke(self, device: Device, *, revoked_by: Optional[UUID] = None) -> Device:
        device.status = DeviceStatus.revoked.value
        device.revoked_at = datetime.now(timezone.utc)
        device.revoked_by = revoked_by
        await self._db.flush()
        return device

    async def mark_offline(self, device: Device) -> Device:
        device.status = DeviceStatus.offline.value
        await self._db.flush()
        return device

    async def list_for_user(self, user_id: UUID) -> list[Device]:
        result = await self._db.execute(
            select(Device)
            .where(Device.user_id == user_id)
            .where(Device.status != DeviceStatus.pending_pairing.value)
            .order_by(Device.created_at.desc())
        )
        return list(result.scalars().all())

    async def list_recent_for_workspace(self, workspace_id: UUID) -> list[Device]:
        """Devices that last ran in this workspace (visibility only — not ownership)."""
        result = await self._db.execute(
            select(Device)
            .where(Device.last_workspace_id == workspace_id)
            .where(Device.status != DeviceStatus.pending_pairing.value)
            .order_by(Device.last_heartbeat_at.desc().nulls_last(), Device.created_at.desc())
        )
        return list(result.scalars().all())

    async def get_most_recent_active_for_user(self, user_id: UUID) -> Optional[Device]:
        result = await self._db.execute(
            select(Device)
            .where(Device.user_id == user_id)
            .where(Device.status == DeviceStatus.active.value)
            .order_by(Device.last_heartbeat_at.desc().nulls_last(), Device.paired_at.desc())
            .limit(1)
        )
        return result.scalar_one_or_none()

    async def list_stale_active(self, cutoff: datetime) -> list[Device]:
        """Devices currently ``active`` with no heartbeat since ``cutoff`` (health monitor)."""
        result = await self._db.execute(
            select(Device)
            .where(Device.status == DeviceStatus.active.value)
            .where(
                (Device.last_heartbeat_at.is_(None) & (Device.paired_at < cutoff))
                | (Device.last_heartbeat_at.isnot(None) & (Device.last_heartbeat_at < cutoff))
            )
        )
        return list(result.scalars().all())


class DeviceEventRepository:
    def __init__(self, db: AsyncSession):
        self._db = db

    async def create(
        self,
        device_id: UUID,
        event_type: str,
        triggered_by: str = "user",
        metadata: Optional[dict] = None,
    ) -> DeviceEvent:
        row = DeviceEvent(
            id=uuid4(),
            device_id=device_id,
            event_type=event_type,
            triggered_by=triggered_by,
            metadata_json=metadata,
        )
        self._db.add(row)
        await self._db.flush()
        return row

    async def list_for_device(self, device_id: UUID, limit: int = 50) -> list[DeviceEvent]:
        result = await self._db.execute(
            select(DeviceEvent)
            .where(DeviceEvent.device_id == device_id)
            .order_by(DeviceEvent.occurred_at.desc())
            .limit(limit)
        )
        return list(result.scalars().all())
