from __future__ import annotations

import hashlib
import secrets
from datetime import UTC, datetime, timedelta
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.models.auth_session import AuthSession
from app.services.tenant_registry import required_tenant_id

DEFAULT_SESSION_TIMEOUT_MINUTES = 30
MIN_SESSION_TIMEOUT_MINUTES = 5
MAX_SESSION_TIMEOUT_MINUTES = 240


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

    def issue(
        self,
        user: Any,
        *,
        metric_session_id: str,
        timeout_minutes: int = DEFAULT_SESSION_TIMEOUT_MINUTES,
    ) -> tuple[str, AuthSession]:
        now = datetime.now(UTC)
        normalized_timeout = max(
            MIN_SESSION_TIMEOUT_MINUTES,
            min(
                MAX_SESSION_TIMEOUT_MINUTES,
                int(timeout_minutes or DEFAULT_SESSION_TIMEOUT_MINUTES),
            ),
        )
        access_token = secrets.token_urlsafe(32)
        tenant_id = required_tenant_id(getattr(user, "tenant_id", None))
        auth_session = AuthSession(
            token_hash=self.hash_token(access_token),
            tenant_id=tenant_id,
            principal_type="admin" if bool(getattr(user, "is_admin", False)) else "employee",
            employee_id=str(getattr(user, "employee_id", "") or "").strip() or None,
            username=str(getattr(user, "username", "") or "").strip(),
            metric_session_id=str(metric_session_id or "").strip(),
            issued_at=now,
            expires_at=now + timedelta(minutes=normalized_timeout),
            last_seen_at=now,
        )
        self.db.add(auth_session)
        self.db.flush()
        return access_token, auth_session

    def authenticate(self, access_token: str) -> AuthSession | None:
        normalized_token = str(access_token or "").strip()
        if not normalized_token:
            return None

        auth_session = self.db.scalars(
            select(AuthSession).where(AuthSession.token_hash == self.hash_token(normalized_token))
        ).first()
        if auth_session is None or auth_session.revoked_at is not None:
            return None

        now = datetime.now(UTC)
        if self._as_utc(auth_session.expires_at) <= now:
            return None

        auth_session.last_seen_at = now
        return auth_session

    def revoke(self, session_id: str, *, commit: bool = True) -> bool:
        normalized_session_id = str(session_id or "").strip()
        if not normalized_session_id:
            return False

        auth_session = self.db.get(AuthSession, normalized_session_id)
        if auth_session is None:
            return False

        if auth_session.revoked_at is None:
            auth_session.revoked_at = datetime.now(UTC)
            if commit:
                self.db.commit()
            else:
                self.db.flush()
        return True

    @staticmethod
    def hash_token(access_token: str) -> str:
        return hashlib.sha256(str(access_token or "").encode("utf-8")).hexdigest()

    @staticmethod
    def _as_utc(value: datetime) -> datetime:
        if value.tzinfo is None:
            return value.replace(tzinfo=UTC)
        return value.astimezone(UTC)
