from __future__ import annotations

import threading
import uuid
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import or_, select
from sqlalchemy.orm import Session

from app.db.base import Base
from app.models.user_session_metric import UserSessionMetric

MAX_SESSION_DURATION_MS = 24 * 60 * 60 * 1000


class UserSessionMetricService:
    _storage_ready_lock = threading.Lock()
    _storage_ready_keys: set[tuple[str, int]] = set()

    def __init__(self, db: Session) -> None:
        self.db = db

    @staticmethod
    def _bind_cache_key(db: Session) -> tuple[str, int]:
        bind = db.get_bind()
        return (bind.url.render_as_string(hide_password=True), id(bind.pool))

    def ensure_storage_ready(self) -> None:
        cache_key = self._bind_cache_key(self.db)
        if cache_key in self._storage_ready_keys:
            return

        with self._storage_ready_lock:
            if cache_key in self._storage_ready_keys:
                return

            self._ensure_storage_ready_uncached()
            self._storage_ready_keys.add(cache_key)

    def _ensure_storage_ready_uncached(self) -> None:
        Base.metadata.create_all(bind=self.db.get_bind(), tables=[UserSessionMetric.__table__])

    def start_session(
        self,
        user: Any,
        *,
        event: dict[str, Any] | None = None,
        commit: bool = True,
    ) -> UserSessionMetric:
        self.ensure_storage_ready()
        now = datetime.now(UTC)
        username = str(getattr(user, "username", "") or getattr(user, "email", "") or "").strip()
        display_name = str(getattr(user, "name", "") or username).strip()
        session = UserSessionMetric(
            session_id=str(uuid.uuid4()),
            username=username,
            display_name=display_name,
            employee_no=str(getattr(user, "employee_no", "") or "").strip(),
            email=str(getattr(user, "email", "") or username).strip(),
            is_admin=bool(getattr(user, "is_admin", False)),
            login_at=now,
            last_activity_at=now,
            status="active",
            event_json=event or {},
        )
        self.db.add(session)
        if commit:
            self.db.commit()
            self.db.refresh(session)
        else:
            self.db.flush()
        return session

    def finish_session(
        self,
        *,
        session_id: str,
        expected_username: str = "",
        reason: str = "manual",
        last_activity_at: datetime | None = None,
        activity_event_count: int = 0,
        event: dict[str, Any] | None = None,
        commit: bool = True,
    ) -> UserSessionMetric | None:
        self.ensure_storage_ready()
        normalized_session_id = str(session_id or "").strip()
        if not normalized_session_id:
            return None

        stmt = select(UserSessionMetric).where(
            UserSessionMetric.session_id == normalized_session_id
        )
        normalized_username = str(expected_username or "").strip()
        if normalized_username:
            stmt = stmt.where(UserSessionMetric.username == normalized_username)
        session = self.db.scalars(stmt).first()
        if session is None:
            return None

        if session.status == "closed":
            return session

        logout_at = datetime.now(UTC)
        session.logout_at = logout_at
        session.last_activity_at = self._normalize_last_activity(
            last_activity_at,
            session.login_at,
            logout_at,
        )
        session.duration_ms = self._duration_ms(session.login_at, logout_at)
        session.activity_event_count = max(0, int(activity_event_count or 0))
        session.logout_reason = str(reason or "manual").strip()[:40] or "manual"
        session.status = "closed"
        session.event_json = {
            **(session.event_json or {}),
            "finish": event or {},
        }
        if commit:
            self.db.commit()
            self.db.refresh(session)
        else:
            self.db.flush()
        return session

    def sum_duration_ms(self, identifiers: set[str], cutoff: datetime) -> int:
        self.ensure_storage_ready()
        normalized = {str(item or "").strip() for item in identifiers if str(item or "").strip()}
        if not normalized:
            return 0

        stmt = select(UserSessionMetric).where(
            UserSessionMetric.status == "closed",
            or_(UserSessionMetric.login_at >= cutoff, UserSessionMetric.logout_at >= cutoff),
            or_(
                UserSessionMetric.username.in_(normalized),
                UserSessionMetric.email.in_(normalized),
                UserSessionMetric.employee_no.in_(normalized),
                UserSessionMetric.display_name.in_(normalized),
            ),
        )
        return sum(max(0, int(item.duration_ms or 0)) for item in self.db.scalars(stmt).all())

    @staticmethod
    def _duration_ms(login_at: datetime | None, logout_at: datetime) -> int:
        if login_at is None:
            return 0
        if login_at.tzinfo is None and logout_at.tzinfo is not None:
            logout_at = logout_at.replace(tzinfo=None)
        elif login_at.tzinfo is not None and logout_at.tzinfo is None:
            logout_at = logout_at.replace(tzinfo=login_at.tzinfo)
        try:
            duration_ms = int((logout_at - login_at).total_seconds() * 1000)
        except TypeError:
            return 0
        return max(0, min(duration_ms, MAX_SESSION_DURATION_MS))

    @staticmethod
    def _normalize_last_activity(
        value: datetime | None,
        login_at: datetime | None,
        logout_at: datetime,
    ) -> datetime:
        if value is None:
            return logout_at
        try:
            if login_at is not None and value < login_at:
                return login_at
            if value > logout_at:
                return logout_at
            return value
        except TypeError:
            return logout_at
