"""Session Manager for AiCippy.

Handles user authentication sessions with persistent sessions (no timeout)
of inactivity or usage. Supports secure token storage, validation,
multi-session (per-device session files), and cross-terminal session sharing.
"""

from __future__ import annotations

import contextlib
import hashlib
import json
import os
import re
import secrets
import sys
import tempfile
import time
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import TYPE_CHECKING, Final

if TYPE_CHECKING:
    from collections.abc import Generator

from aicippy.installer.platform_detect import PlatformInfo, detect_platform
from aicippy.utils.logging import get_logger

# fcntl is Unix-only; on Windows we fall back to no-op locking.
# The atomic temp-file rename already provides basic write safety on Windows.
if sys.platform != "win32":
    import fcntl
else:
    fcntl = None  # type: ignore[assignment]

logger = get_logger(__name__)

# Session configuration
SESSION_TIMEOUT_MINUTES: Final[int] = 1440  # 24 hours
SESSION_DIR_NAME: Final[str] = "sessions"
TOKEN_LENGTH: Final[int] = 32

# Session states
SESSION_VALID: Final[str] = "valid"
SESSION_EXPIRED: Final[str] = "expired"
SESSION_INVALID: Final[str] = "invalid"
SESSION_MISSING: Final[str] = "missing"

# File size sanity limits
_MAX_SESSION_FILE_BYTES: Final[int] = 1_048_576  # 1 MB

# Device ID must be lowercase hex only (output of sha256 hex digest, truncated)
_DEVICE_ID_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]+$")


@dataclass
class SessionInfo:
    """User session information."""

    user_id: str
    username: str
    email: str
    session_token: str
    created_at: datetime
    last_activity: datetime
    expires_at: datetime
    device_id: str
    tenant_id: str = ""
    ip_address: str | None = None
    metadata: dict[str, object] = field(default_factory=dict)

    def to_dict(self) -> dict:
        """Convert session to dictionary for storage."""
        return {
            "user_id": self.user_id,
            "username": self.username,
            "email": self.email,
            "session_token": self.session_token,
            "created_at": self.created_at.isoformat(),
            "last_activity": self.last_activity.isoformat(),
            "expires_at": self.expires_at.isoformat(),
            "device_id": self.device_id,
            "tenant_id": self.tenant_id,
            "ip_address": self.ip_address,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict) -> SessionInfo:
        """Create session from dictionary."""
        return cls(
            user_id=data["user_id"],
            username=data["username"],
            email=data["email"],
            session_token=data["session_token"],
            created_at=datetime.fromisoformat(data["created_at"]),
            last_activity=datetime.fromisoformat(data["last_activity"]),
            expires_at=datetime.fromisoformat(data["expires_at"]),
            device_id=data["device_id"],
            tenant_id=data.get("tenant_id", ""),
            ip_address=data.get("ip_address"),
            metadata=data.get("metadata", {}),
        )

    @property
    def is_expired(self) -> bool:
        """Check if session has expired based on expires_at timestamp."""
        return datetime.now(UTC) > self.expires_at

    @property
    def time_remaining(self) -> timedelta:
        """Get time remaining until expiration."""
        now = datetime.now(UTC)
        remaining = self.expires_at - now
        return remaining if remaining.total_seconds() > 0 else timedelta(0)

    @property
    def minutes_remaining(self) -> float:
        """Get minutes remaining until expiration."""
        return self.time_remaining.total_seconds() / 60

    @property
    def hours_remaining(self) -> float:
        """Get hours remaining until expiration."""
        return self.time_remaining.total_seconds() / 3600


@dataclass
class SessionValidationResult:
    """Result of session validation."""

    status: str
    session: SessionInfo | None
    message: str
    needs_login: bool


def generate_session_token() -> str:
    """Generate a secure random session token."""
    return secrets.token_hex(TOKEN_LENGTH)


def generate_device_id() -> str:
    """Generate a unique device identifier based on system info."""
    platform_info = detect_platform()

    # Combine system identifiers
    components = [
        platform_info.os_name,
        platform_info.os_version,
        str(platform_info.architecture),
        platform_info.home_dir.name,
        os.environ.get("USER", os.environ.get("USERNAME", "unknown")),
    ]

    # Create a hash
    combined = "|".join(components)
    return hashlib.sha256(combined.encode()).hexdigest()[:16]


def _get_sessions_dir(
    platform_info: PlatformInfo | None = None,
) -> Path:
    """Get the sessions directory path.

    Args:
        platform_info: Optional platform info (will be detected if not
            provided).

    Returns:
        Path to the sessions directory.

    """
    if platform_info is None:
        platform_info = detect_platform()

    return platform_info.config_path / SESSION_DIR_NAME


def _get_device_session_path(
    device_id: str,
    platform_info: PlatformInfo | None = None,
) -> Path:
    """Get the session file path for a specific device.

    Args:
        device_id: The device identifier (must be lowercase hex).
        platform_info: Optional platform info.

    Returns:
        Path to the device-specific session file.

    Raises:
        ValueError: If device_id contains non-hex characters
            (path traversal prevention).

    """
    if not _DEVICE_ID_PATTERN.match(device_id):
        msg = f"Invalid device_id: must be lowercase hex, got '{device_id}'"
        raise ValueError(msg)
    sessions_dir = _get_sessions_dir(platform_info)
    return sessions_dir / f"session_{device_id}.json"


def get_session_file_path(
    platform_info: PlatformInfo | None = None,
) -> Path:
    """Get the path to the session file for the current device.

    Args:
        platform_info: Optional platform info (will be detected if
            not provided).

    Returns:
        Path to the session file.

    """
    device_id = generate_device_id()
    return _get_device_session_path(device_id, platform_info)


@contextlib.contextmanager
def _session_file_lock(
    session_path: Path,
    *,
    exclusive: bool = True,
) -> Generator[None]:
    """Acquire a file-level lock for cross-process safety.

    Uses a separate .lock file alongside the session file.

    On Windows (where fcntl is unavailable), returns a no-op context
    manager. The atomic temp-file rename provides basic write safety.

    Args:
        session_path: Path to the session file being protected.
        exclusive: True for write operations (LOCK_EX),
            False for read (LOCK_SH).

    Yields:
        None while the lock is held.

    """
    if fcntl is None:
        # Windows fallback: no-op
        yield
        return

    lock_path = session_path.with_suffix(".lock")
    lock_fd = None
    try:
        lock_path.parent.mkdir(parents=True, exist_ok=True)
        lock_fd = lock_path.open("w")
        lock_op = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
        fcntl.flock(lock_fd, lock_op)
        yield
    finally:
        if lock_fd is not None:
            try:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
            finally:
                lock_fd.close()


def _quarantine_corrupted_file(file_path: Path) -> None:
    """Rename a corrupted session file as a backup for debugging.

    The file is renamed to ``<name>.corrupted.<timestamp>`` so the
    original data is preserved for analysis while the path is freed
    for a fresh session.

    Args:
        file_path: Path to the corrupted file.

    """
    try:
        ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%S")
        backup = file_path.with_suffix(f".corrupted.{ts}")
        file_path.rename(backup)
        logger.warning(
            "session_file_quarantined",
            original=str(file_path),
            backup=str(backup),
        )
    except OSError:
        logger.warning(
            "session_file_quarantine_failed",
            path=str(file_path),
            exc_info=True,
        )


def _write_session_file(
    session_path: Path,
    session_json: str,
) -> None:
    """Write session JSON to disk atomically with verification.

    Writes to a temp file, renames into place, then reads back and
    verifies the content. If verification fails, retries once.

    All file descriptors and temp files are cleaned up in every code
    path via try/finally.

    Args:
        session_path: Destination path.
        session_json: Serialized JSON string to write.

    Raises:
        OSError: If the write fails after retry.
        RuntimeError: If write verification fails after retry.

    """
    for attempt in range(2):
        tmp_path: str | None = None
        fd: int = -1
        try:
            if os.name != "nt":
                fd, tmp_path = tempfile.mkstemp(
                    dir=str(session_path.parent),
                    prefix=".session_",
                    suffix=".tmp",
                )
                try:
                    os.fchmod(fd, 0o600)
                    os.write(fd, session_json.encode("utf-8"))
                finally:
                    if fd >= 0:
                        os.close(fd)
                        fd = -1
                Path(tmp_path).rename(session_path)
            else:
                fd, tmp_path = tempfile.mkstemp(
                    dir=str(session_path.parent),
                    prefix=".session_",
                    suffix=".tmp",
                )
                try:
                    os.write(fd, session_json.encode("utf-8"))
                finally:
                    if fd >= 0:
                        os.close(fd)
                        fd = -1
                Path(tmp_path).replace(session_path)

            # Mark tmp_path as consumed (rename/replace succeeded)
            tmp_path = None

            def _verify_write() -> None:
                # --- Write verification ---
                read_back = session_path.read_text(encoding="utf-8")
                parsed = json.loads(read_back)
                expected = json.loads(session_json)
                if parsed != expected:
                    msg = "Write verification mismatch"
                    raise RuntimeError(msg)  # noqa: TRY301

            _verify_write()

        except BaseException:
            # Clean up temp file if it was not consumed
            if tmp_path is not None:
                try:
                    Path(tmp_path).unlink(missing_ok=True)
                except OSError:
                    logger.warning(
                        "session_tmp_cleanup_failed",
                        path=tmp_path,
                        exc_info=True,
                    )
            if attempt == 0:
                logger.warning(
                    "session_write_failed_retrying",
                    path=str(session_path),
                    exc_info=True,
                )
                continue
            raise
        else:
            # Success
            return


def _read_session_file(
    session_path: Path,
) -> SessionInfo | None:
    """Read and parse a session file with full safety checks.

    Checks:
    - File existence
    - File is readable (permission check)
    - File size sanity (0 bytes or > 1 MB treated as corrupt)
    - Valid JSON
    - Valid SessionInfo structure

    On corruption, the file is quarantined (renamed with a
    ``.corrupted.<timestamp>`` suffix) and ``None`` is returned.

    Args:
        session_path: Path to the session file.

    Returns:
        SessionInfo if valid, None otherwise.

    """
    if not session_path.exists():
        return None

    # Permission check
    if not os.access(session_path, os.R_OK):
        logger.warning(
            "session_file_not_readable",
            path=str(session_path),
            mode=oct(session_path.stat().st_mode) if session_path.exists() else "N/A",
        )
        return None

    # File size sanity check
    try:
        file_size = session_path.stat().st_size
    except OSError:
        logger.warning(
            "session_file_stat_failed",
            path=str(session_path),
            exc_info=True,
        )
        return None

    if file_size == 0:
        logger.warning(
            "session_file_empty",
            path=str(session_path),
            size=file_size,
        )
        _quarantine_corrupted_file(session_path)
        return None

    if file_size > _MAX_SESSION_FILE_BYTES:
        logger.warning(
            "session_file_too_large",
            path=str(session_path),
            size=file_size,
            max_size=_MAX_SESSION_FILE_BYTES,
        )
        _quarantine_corrupted_file(session_path)
        return None

    # Read and parse
    try:
        raw = session_path.read_text(encoding="utf-8")
    except OSError:
        logger.warning(
            "session_file_read_failed",
            path=str(session_path),
            exc_info=True,
        )
        return None

    try:
        data = json.loads(raw)
    except (json.JSONDecodeError, ValueError) as exc:
        logger.warning(
            "session_file_json_corrupt",
            path=str(session_path),
            error=str(exc),
            file_size=file_size,
        )
        _quarantine_corrupted_file(session_path)
        return None

    try:
        return SessionInfo.from_dict(data)
    except (KeyError, TypeError, ValueError) as exc:
        logger.warning(
            "session_file_schema_invalid",
            path=str(session_path),
            error=str(exc),
        )
        _quarantine_corrupted_file(session_path)
        return None


def _migrate_old_session(
    platform_info: PlatformInfo | None = None,
) -> None:
    """Migrate old-style session.json to per-device sessions directory.

    If a legacy session.json exists directly in the config directory
    (not in sessions/), move it to sessions/session_{device_id}.json.

    The old file is only removed after the new file is verified.

    Args:
        platform_info: Optional platform info.

    """
    if platform_info is None:
        platform_info = detect_platform()

    old_session = platform_info.config_path / "session.json"
    if not old_session.exists():
        return

    logger.info(
        "session_migration_starting",
        old_path=str(old_session),
    )

    sessions_dir = _get_sessions_dir(platform_info)
    sessions_dir.mkdir(parents=True, exist_ok=True)

    device_id = generate_device_id()
    new_path = sessions_dir / f"session_{device_id}.json"

    try:
        if not new_path.exists():
            # Read old file content
            old_content = old_session.read_text(encoding="utf-8")

            # Validate it is parseable before writing
            json.loads(old_content)

            # Write to new location and verify
            _write_session_file(new_path, old_content)

            # Only delete old file after new one is verified
            old_session.unlink(missing_ok=True)
            logger.info(
                "session_migration_complete",
                old_path=str(old_session),
                new_path=str(new_path),
            )
        else:
            # New session already exists; remove old file
            old_session.unlink(missing_ok=True)
            logger.info(
                "session_migration_skipped_exists",
                new_path=str(new_path),
            )
    except Exception:
        logger.warning(
            "session_migration_failed",
            old_path=str(old_session),
            new_path=str(new_path),
            exc_info=True,
        )


def create_session(
    user_id: str,
    username: str,
    email: str,
    ip_address: str | None = None,
    metadata: dict[str, object] | None = None,
) -> SessionInfo:
    """Create a new user session.

    Args:
        user_id: Unique user identifier.
        username: User's display name.
        email: User's email address.
        ip_address: Optional IP address.
        metadata: Optional additional metadata.

    Returns:
        New SessionInfo instance.

    """
    now = datetime.now(UTC)
    expires_at = now + timedelta(minutes=SESSION_TIMEOUT_MINUTES)

    return SessionInfo(
        user_id=user_id,
        username=username,
        email=email,
        session_token=generate_session_token(),
        created_at=now,
        last_activity=now,
        expires_at=expires_at,
        device_id=generate_device_id(),
        ip_address=ip_address,
        metadata=metadata or {},
    )


def save_session(
    session: SessionInfo,
    platform_info: PlatformInfo | None = None,
) -> tuple[bool, str]:
    """Save session to secure storage (per-device session file).

    Args:
        session: Session to save.
        platform_info: Optional platform info.

    Returns:
        Tuple of (success, message).

    """
    try:
        session_path = get_session_file_path(platform_info)

        # Ensure directory exists with restricted permissions
        session_path.parent.mkdir(parents=True, exist_ok=True)
        if os.name != "nt":
            try:
                session_path.parent.chmod(0o700)
            except OSError:
                logger.warning(
                    "session_dir_chmod_failed",
                    path=str(session_path.parent),
                    exc_info=True,
                )

        # Serialize session data
        session_data = session.to_dict()
        session_json = json.dumps(session_data, indent=2, default=str)

        # Write atomically with verification
        _write_session_file(session_path, session_json)

        return True, f"Session saved for {session.username}"

    except PermissionError:
        logger.warning(
            "session_save_permission_denied",
            exc_info=True,
        )
        return False, "Permission denied when saving session"
    except Exception as exc:
        logger.warning(
            "session_save_failed",
            error=str(exc),
            exc_info=True,
        )
        return False, f"Failed to save session: {exc}"


def load_session(
    platform_info: PlatformInfo | None = None,
) -> SessionInfo | None:
    """Load session from storage for the current device.

    Args:
        platform_info: Optional platform info.

    Returns:
        SessionInfo if found, None otherwise.

    """
    session_path = get_session_file_path(platform_info)
    return _read_session_file(session_path)


def list_all_sessions(
    platform_info: PlatformInfo | None = None,
) -> list[SessionInfo]:
    """List all active (non-expired) sessions across all terminals.

    Args:
        platform_info: Optional platform info.

    Returns:
        List of active SessionInfo instances.

    """
    sessions_dir = _get_sessions_dir(platform_info)
    if not sessions_dir.exists():
        return []

    sessions: list[SessionInfo] = []
    for f in sessions_dir.glob("session_*.json"):
        info = _read_session_file(f)
        if info is not None and not info.is_expired:
            sessions.append(info)
    return sessions


def find_active_session_for_email(
    email: str,
    platform_info: PlatformInfo | None = None,
) -> SessionInfo | None:
    """Find any active session for a given email across all terminals.

    Args:
        email: The email to search for.
        platform_info: Optional platform info.

    Returns:
        SessionInfo if found, None otherwise.

    """
    for session in list_all_sessions(platform_info):
        if session.email == email and not session.is_expired:
            return session
    return None


def has_any_active_session(
    platform_info: PlatformInfo | None = None,
) -> SessionInfo | None:
    """Check if ANY active session exists (any email, any terminal).

    Args:
        platform_info: Optional platform info.

    Returns:
        The most recently active SessionInfo if found, None otherwise.

    """
    sessions = list_all_sessions(platform_info)
    if sessions:
        # Return the most recently active session
        return max(sessions, key=lambda s: s.last_activity)
    return None


def cleanup_expired_sessions(
    platform_info: PlatformInfo | None = None,
) -> int:
    """Remove expired session files. Returns count removed.

    Args:
        platform_info: Optional platform info.

    Returns:
        Number of expired session files removed.

    """
    sessions_dir = _get_sessions_dir(platform_info)
    if not sessions_dir.exists():
        return 0

    removed = 0
    for f in sessions_dir.glob("session_*.json"):
        try:
            data = json.loads(f.read_text(encoding="utf-8"))
            info = SessionInfo.from_dict(data)
            if info.is_expired:
                f.unlink()
                removed += 1
        except Exception:
            logger.warning(
                "session_file_cleanup_error",
                path=str(f),
                exc_info=True,
            )
            f.unlink(missing_ok=True)
            removed += 1
    return removed


def validate_session(
    platform_info: PlatformInfo | None = None,
) -> SessionValidationResult:
    """Validate the current session.

    Checks:
    1. Current device's session file
    2. If not found/expired, checks other terminals for an active
       session and adopts it (cross-terminal session sharing)
    3. Returns missing/expired if no active session anywhere

    Args:
        platform_info: Optional platform info.

    Returns:
        SessionValidationResult with status and details.

    """
    # Migrate old-style session.json if present
    _migrate_old_session(platform_info)

    # 1. Check current device's session
    session = load_session(platform_info)

    if session is not None and not session.is_expired:
        # Session is valid on this device
        minutes_left = session.minutes_remaining
        return SessionValidationResult(
            status=SESSION_VALID,
            session=session,
            message=(f"Welcome back, {session.username}! Session valid for {minutes_left:.0f} more minutes."),
            needs_login=False,
        )

    # 2. Check other terminals for an active session
    other_session = has_any_active_session(platform_info)
    if other_session is not None:
        # Validate tenant_id matches if the current device had a previous
        # session with a different tenant (prevents cross-tenant adoption).
        if (
            session is not None
            and session.tenant_id
            and other_session.tenant_id
            and session.tenant_id != other_session.tenant_id
        ):
            logger.warning(
                "session_adoption_tenant_mismatch",
                current_tenant=session.tenant_id,
                other_tenant=other_session.tenant_id,
            )
            # Treat as no active session — force re-login
            return SessionValidationResult(
                status=SESSION_INVALID,
                session=None,
                message="Tenant mismatch between sessions. Please login again.",
                needs_login=True,
            )

        # Adopt session for this device under file lock to prevent
        # race conditions when multiple terminals adopt simultaneously.
        session_path = get_session_file_path(platform_info)
        with _session_file_lock(session_path, exclusive=True):
            adopted = SessionInfo(
                user_id=other_session.user_id,
                username=other_session.username,
                email=other_session.email,
                session_token=other_session.session_token,
                created_at=other_session.created_at,
                last_activity=datetime.now(UTC),
                expires_at=other_session.expires_at,
                device_id=generate_device_id(),
                tenant_id=other_session.tenant_id,
                ip_address=other_session.ip_address,
                metadata=other_session.metadata,
            )
            save_session(adopted, platform_info)

        logger.info(
            "session_adopted_from_terminal",
            username=adopted.username,
            email=adopted.email,
            source_device=other_session.device_id,
            target_device=adopted.device_id,
        )

        return SessionValidationResult(
            status=SESSION_VALID,
            session=adopted,
            message=(f"Session adopted from another terminal. Welcome back, {adopted.username}!"),
            needs_login=False,
        )

    # 3. No active session anywhere
    if session is not None and session.is_expired:
        return SessionValidationResult(
            status=SESSION_EXPIRED,
            session=session,
            message=(f"Session expired. Last activity was {session.last_activity.strftime('%Y-%m-%d %H:%M:%S')} UTC."),
            needs_login=True,
        )

    return SessionValidationResult(
        status=SESSION_MISSING,
        session=None,
        message="No active session found. Please login.",
        needs_login=True,
    )


def refresh_session(
    platform_info: PlatformInfo | None = None,
) -> tuple[bool, str]:
    """Refresh the current session's activity timestamp and extend
    expiration.

    Args:
        platform_info: Optional platform info.

    Returns:
        Tuple of (success, message).

    """  # noqa: D205
    session = load_session(platform_info)

    if session is None:
        return False, "No session to refresh"

    if session.is_expired:
        return False, "Session has expired and cannot be refreshed"

    # Update activity and extend expiration
    now = datetime.now(UTC)
    session.last_activity = now
    session.expires_at = now + timedelta(minutes=SESSION_TIMEOUT_MINUTES)

    return save_session(session, platform_info)


def invalidate_session(
    platform_info: PlatformInfo | None = None,
) -> tuple[bool, str]:
    """Invalidate (logout) the current session.

    Args:
        platform_info: Optional platform info.

    Returns:
        Tuple of (success, message).

    """
    try:
        session_path = get_session_file_path(platform_info)

        if session_path.exists():
            session_path.unlink()
            return True, "Successfully logged out"

        return True, "No active session"

    except PermissionError:
        return False, "Permission denied when removing session"
    except Exception as exc:
        return False, f"Failed to invalidate session: {exc}"


def get_session_status_display(
    platform_info: PlatformInfo | None = None,
) -> str:
    """Get a formatted status display of the current session.

    Args:
        platform_info: Optional platform info.

    Returns:
        Formatted status string.

    """
    result = validate_session(platform_info)

    if result.status == SESSION_MISSING:
        return "Not logged in"

    if result.status == SESSION_EXPIRED:
        return f"Session expired (was: {getattr(result.session, 'username', 'Unknown')})"

    if result.status == SESSION_INVALID:
        return "Invalid session"

    session = result.session
    if not session:
        return "Invalid session"
    minutes_left = session.minutes_remaining

    if minutes_left < 1:
        seconds_left = int(session.time_remaining.total_seconds())
        time_str = f"{seconds_left} seconds"
    else:
        time_str = f"{minutes_left:.0f} minutes"

    return f"Logged in as {session.username} ({time_str} remaining)"


def needs_login(
    platform_info: PlatformInfo | None = None,
) -> bool:
    """Check if login is required.

    Args:
        platform_info: Optional platform info.

    Returns:
        True if login is needed.

    """
    result = validate_session(platform_info)
    return result.needs_login


def get_current_user(
    platform_info: PlatformInfo | None = None,
) -> tuple[str | None, str | None]:
    """Get current logged-in user info.

    Args:
        platform_info: Optional platform info.

    Returns:
        Tuple of (username, email) or (None, None) if not logged in.

    """
    result = validate_session(platform_info)

    if result.status == SESSION_VALID and result.session:
        return result.session.username, result.session.email

    return None, None


class SessionManager:
    """High-level session manager for the AiCippy CLI.

    Provides a convenient interface for session operations with
    automatic platform detection, caching, multi-session support,
    and cross-terminal session sharing. Sessions expire after 60
    minutes.
    """

    def __init__(self, platform_info: PlatformInfo | None = None) -> None:
        """Initialize session manager with backward-compatible migration."""
        self._platform_info = platform_info or detect_platform()
        self._cached_session: SessionInfo | None = None
        self._last_check: float = 0
        self._check_interval: float = 60.0  # Re-check every 60s
        self._device_id = generate_device_id()

        # Ensure sessions directory exists with restricted permissions
        sessions_dir = _get_sessions_dir(self._platform_info)
        sessions_dir.mkdir(parents=True, exist_ok=True)
        if os.name != "nt":
            try:
                sessions_dir.chmod(0o700)
            except OSError:
                logger.warning(
                    "sessions_dir_chmod_failed",
                    path=str(sessions_dir),
                    exc_info=True,
                )

        # Migrate old-style session.json if present
        _migrate_old_session(self._platform_info)

    @property
    def platform_info(self) -> PlatformInfo:
        """Get platform information."""
        return self._platform_info

    @property
    def device_id(self) -> str:
        """Get the device identifier for the current terminal."""
        return self._device_id

    def _should_recheck(self) -> bool:
        """Check if we should re-validate the session."""
        return time.monotonic() - self._last_check > self._check_interval

    def login(
        self,
        user_id: str,
        username: str,
        email: str,
        ip_address: str | None = None,
        metadata: dict[str, object] | None = None,
    ) -> tuple[bool, str]:
        """Create a new login session.

        Args:
            user_id: User's unique ID.
            username: User's display name.
            email: User's email.
            ip_address: Optional IP address.
            metadata: Optional metadata.

        Returns:
            Tuple of (success, message).

        """
        session = create_session(
            user_id=user_id,
            username=username,
            email=email,
            ip_address=ip_address,
            metadata=metadata,
        )

        success, message = save_session(session, self._platform_info)

        if success:
            self._cached_session = session
            self._last_check = time.monotonic()

        return success, message

    def logout(self) -> tuple[bool, str]:
        """Logout the current session.

        Returns:
            Tuple of (success, message).

        """
        success, message = invalidate_session(self._platform_info)

        if success:
            self._cached_session = None

        return success, message

    def validate(self) -> SessionValidationResult:
        """Validate the current session.

        Checks:
        1. Cached session (if recent)
        2. Current device session file
        3. Other terminals for cross-terminal adoption

        Returns:
            SessionValidationResult with status.

        """
        # Use cached session if recent and not expired
        if self._cached_session and not self._should_recheck() and not self._cached_session.is_expired:
            # Cache staleness check: verify the file still exists
            # on disk. If deleted externally, invalidate cache.
            session_path = get_session_file_path(self._platform_info)
            if session_path.exists():
                return SessionValidationResult(
                    status=SESSION_VALID,
                    session=self._cached_session,
                    message=(f"Welcome back, {self._cached_session.username}!"),
                    needs_login=False,
                )
            # File was deleted externally; invalidate cache
            logger.info(
                "session_cache_invalidated_file_deleted",
                path=str(session_path),
            )
            self._cached_session = None

        # Full validation (includes cross-terminal check)
        result = validate_session(self._platform_info)
        self._last_check = time.monotonic()

        if result.status == SESSION_VALID:
            self._cached_session = result.session
        else:
            self._cached_session = None

        return result

    def refresh(self) -> tuple[bool, str]:
        """Refresh the current session.

        Returns:
            Tuple of (success, message).

        """
        success, message = refresh_session(self._platform_info)

        if success:
            self._cached_session = load_session(self._platform_info)
            self._last_check = time.monotonic()

        return success, message

    def needs_login(self) -> bool:
        """Check if login is required.

        Returns:
            True if login needed.

        """
        return self.validate().needs_login

    def get_user(self) -> tuple[str | None, str | None]:
        """Get current user info.

        Returns:
            Tuple of (username, email).

        """
        result = self.validate()

        if result.status == SESSION_VALID and result.session:
            return result.session.username, result.session.email

        return None, None

    def get_status(self) -> str:
        """Get formatted session status.

        Returns:
            Status string.

        """
        return get_session_status_display(self._platform_info)

    def list_sessions(self) -> list[SessionInfo]:
        """List all active (non-expired) sessions across all terminals.

        Returns:
            List of active SessionInfo instances.

        """
        return list_all_sessions(self._platform_info)

    def cleanup_expired(self) -> int:
        """Remove expired session files.

        Returns:
            Count of removed session files.

        """
        return cleanup_expired_sessions(self._platform_info)

    def find_active_session_for_email(self, email: str) -> SessionInfo | None:
        """Find any active session for a given email across terminals.

        Args:
            email: The email to search for.

        Returns:
            SessionInfo if found, None otherwise.

        """
        return find_active_session_for_email(email, self._platform_info)

    def has_any_active_session(self) -> SessionInfo | None:
        """Check if ANY active session exists (any email, any terminal).

        Returns:
            The most recently active SessionInfo if found,
            None otherwise.

        """
        return has_any_active_session(self._platform_info)

    def touch(self) -> None:
        """Update session activity without full refresh."""
        if self._cached_session and not self._cached_session.is_expired:
            self._cached_session.last_activity = datetime.now(UTC)
            # Save periodically (every 5 minutes of activity)
            if self._should_recheck():
                self.refresh()
