"""
会话服务

负责 refresh token 会话的创建、轮换和撤销。
当前只提供基础能力，后续由 auth API 接入。
"""

from __future__ import annotations

import hashlib
import logging
import uuid
from datetime import datetime, timedelta
from typing import Optional

from sqlmodel import Session, select

from src.auth.jwt import REFRESH_TOKEN_EXPIRE_DAYS
from src.config.database import engine
from src.models.session import UserSessionDB

logger = logging.getLogger(__name__)


class SessionService:
    """用户会话服务"""

    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    @staticmethod
    def _now() -> float:
        return datetime.now().timestamp()

    @staticmethod
    def _default_expiry() -> float:
        return (datetime.now() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)).timestamp()

    @staticmethod
    def hash_refresh_token(refresh_token: str) -> str:
        return hashlib.sha256(refresh_token.encode("utf-8")).hexdigest()

    def create_session(
        self,
        user_id: str,
        refresh_token: str,
        *,
        session_id: Optional[str] = None,
        expires_at: Optional[float] = None,
        session_family_id: Optional[str] = None,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None,
        device_name: Optional[str] = None,
    ) -> UserSessionDB:
        now = self._now()
        session_db = UserSessionDB(
            id=session_id or str(uuid.uuid4()),
            user_id=user_id,
            session_family_id=session_family_id or str(uuid.uuid4()),
            refresh_token_hash=self.hash_refresh_token(refresh_token),
            status="active",
            created_at=now,
            updated_at=now,
            expires_at=expires_at or self._default_expiry(),
            ip_address=ip_address,
            user_agent=user_agent,
            device_name=device_name,
        )

        with Session(engine) as session:
            session.add(session_db)
            session.commit()
            session.refresh(session_db)

        return session_db

    def get_session(self, session_id: str) -> Optional[UserSessionDB]:
        with Session(engine) as session:
            return session.get(UserSessionDB, session_id)

    def get_active_session(self, session_id: str) -> Optional[UserSessionDB]:
        with Session(engine) as session:
            return session.exec(
                select(UserSessionDB).where(
                    UserSessionDB.id == session_id,
                    UserSessionDB.status == "active",
                )
            ).first()

    def list_user_sessions(self, user_id: str, include_inactive: bool = False) -> list[UserSessionDB]:
        with Session(engine) as session:
            query = select(UserSessionDB).where(UserSessionDB.user_id == user_id)
            if not include_inactive:
                query = query.where(UserSessionDB.status == "active")
            query = query.order_by(UserSessionDB.updated_at.desc())
            return list(session.exec(query).all())

    def is_session_active(self, session_id: str) -> bool:
        session_db = self.get_active_session(session_id)
        if not session_db:
            return False
        if session_db.expires_at <= self._now():
            return False
        return True

    def validate_refresh_token(self, session_id: str, refresh_token: str) -> Optional[UserSessionDB]:
        session_db = self.get_active_session(session_id)
        if not session_db:
            return None

        if session_db.expires_at <= self._now():
            self.revoke_session(session_id, reason="expired")
            return None

        expected_hash = self.hash_refresh_token(refresh_token)
        if session_db.refresh_token_hash != expected_hash:
            self.revoke_session_family(session_db.session_family_id, reason="refresh_reuse_detected")
            return None

        return session_db

    def rotate_refresh_token(
        self,
        session_id: str,
        current_refresh_token: str,
        new_refresh_token: str,
        *,
        new_session_id: Optional[str] = None,
        expires_at: Optional[float] = None,
        ip_address: Optional[str] = None,
        user_agent: Optional[str] = None,
        device_name: Optional[str] = None,
    ) -> Optional[UserSessionDB]:
        current_session = self.validate_refresh_token(session_id, current_refresh_token)
        if not current_session:
            return None

        replacement_session = self.create_session(
            user_id=current_session.user_id,
            refresh_token=new_refresh_token,
            session_id=new_session_id,
            expires_at=expires_at,
            session_family_id=current_session.session_family_id,
            ip_address=ip_address or current_session.ip_address,
            user_agent=user_agent or current_session.user_agent,
            device_name=device_name or current_session.device_name,
        )

        now = self._now()
        with Session(engine) as session:
            session_db = session.get(UserSessionDB, session_id)
            if not session_db:
                return replacement_session

            session_db.status = "rotated"
            session_db.updated_at = now
            session_db.last_used_at = now
            session_db.revoked_at = now
            session_db.revoked_reason = "rotated"
            session_db.replaced_by_session_id = replacement_session.id
            session.add(session_db)
            session.commit()

        return replacement_session

    def touch_session(self, session_id: str) -> None:
        now = self._now()
        with Session(engine) as session:
            session_db = session.get(UserSessionDB, session_id)
            if not session_db:
                return
            session_db.last_used_at = now
            session_db.updated_at = now
            session.add(session_db)
            session.commit()

    def revoke_session(self, session_id: str, reason: str = "manual") -> bool:
        now = self._now()
        with Session(engine) as session:
            session_db = session.get(UserSessionDB, session_id)
            if not session_db:
                return False

            if session_db.status != "active":
                return True

            session_db.status = "revoked"
            session_db.updated_at = now
            session_db.revoked_at = now
            session_db.revoked_reason = reason
            session.add(session_db)
            session.commit()
            return True

    def revoke_user_session(self, user_id: str, session_id: str, reason: str = "manual") -> bool:
        with Session(engine) as session:
            session_db = session.get(UserSessionDB, session_id)
            if not session_db or session_db.user_id != user_id:
                return False

        return self.revoke_session(session_id, reason=reason)

    def revoke_session_family(self, session_family_id: str, reason: str = "manual") -> int:
        now = self._now()
        revoked_count = 0

        with Session(engine) as session:
            sessions = session.exec(
                select(UserSessionDB).where(
                    UserSessionDB.session_family_id == session_family_id,
                    UserSessionDB.status == "active",
                )
            ).all()

            for session_db in sessions:
                session_db.status = "revoked"
                session_db.updated_at = now
                session_db.revoked_at = now
                session_db.revoked_reason = reason
                session.add(session_db)
                revoked_count += 1

            session.commit()

        return revoked_count

    def revoke_user_sessions(self, user_id: str, reason: str = "manual") -> int:
        now = self._now()
        revoked_count = 0

        with Session(engine) as session:
            sessions = session.exec(
                select(UserSessionDB).where(
                    UserSessionDB.user_id == user_id,
                    UserSessionDB.status == "active",
                )
            ).all()

            for session_db in sessions:
                session_db.status = "revoked"
                session_db.updated_at = now
                session_db.revoked_at = now
                session_db.revoked_reason = reason
                session.add(session_db)
                revoked_count += 1

            session.commit()

        return revoked_count


session_service = SessionService()
