"""
Session Manager for AiCippy.

Handles user authentication sessions with automatic timeout after 60 minutes
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 hashlib
import json
import os
import re
import secrets
import tempfile
import time
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Final

from aicippy.installer.platform_detect import PlatformInfo, detect_platform

# Session configuration
SESSION_TIMEOUT_MINUTES: Final[int] = 60
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"

# 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
    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,
            "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"],
            ip_address=data.get("ip_address"),
            metadata=data.get("metadata", {}),
        )

    @property
    def is_expired(self) -> bool:
        """Check if session has expired."""
        now = datetime.now(timezone.utc)
        return now >= self.expires_at

    @property
    def time_remaining(self) -> timedelta:
        """Get time remaining until expiration."""
        now = datetime.now(timezone.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):
        raise ValueError(f"Invalid device_id: must be lowercase hex, got '{device_id}'")
    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)


def _migrate_old_session(platform_info: PlatformInfo | None = None) -> None:
    """
    Migrate old-style session.json to the new 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.

    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

    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:
        # Only migrate if the new file does not already exist
        if not new_path.exists():
            old_session.rename(new_path)
        else:
            # New session exists; remove old file
            old_session.unlink()
    except Exception:
        # Best-effort migration; do not block on failure
        pass


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(timezone.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:
                os.chmod(session_path.parent, 0o700)
            except OSError:
                pass

        # Write session data atomically: write to temp file, then rename.
        # This prevents partial/corrupted session files if the process
        # crashes mid-write (rename is atomic on the same filesystem).
        session_data = session.to_dict()
        session_json = json.dumps(session_data, indent=2, default=str)

        if os.name != "nt":
            # Unix: write to a temp file with restrictive permissions,
            # then atomically rename into place.
            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"))
                os.close(fd)
                fd = -1  # Mark as closed
                os.rename(tmp_path, str(session_path))
            except BaseException:
                if fd >= 0:
                    os.close(fd)
                # Clean up temp file on failure
                try:
                    os.unlink(tmp_path)
                except OSError:
                    pass
                raise
        else:
            # Windows: write to temp file then replace.
            # os.replace() is atomic on Windows when on the same volume.
            tmp_fd, tmp_path = tempfile.mkstemp(
                dir=str(session_path.parent),
                prefix=".session_",
                suffix=".tmp",
            )
            try:
                os.write(tmp_fd, session_json.encode("utf-8"))
                os.close(tmp_fd)
                tmp_fd = -1
                os.replace(tmp_path, str(session_path))
            except BaseException:
                if tmp_fd >= 0:
                    os.close(tmp_fd)
                try:
                    os.unlink(tmp_path)
                except OSError:
                    pass
                raise

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

    except PermissionError:
        return False, "Permission denied when saving session"
    except Exception as e:
        return False, f"Failed to save session: {e}"


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.
    """
    try:
        session_path = get_session_file_path(platform_info)

        if not session_path.exists():
            return None

        session_data = json.loads(session_path.read_text())
        return SessionInfo.from_dict(session_data)

    except (json.JSONDecodeError, KeyError, ValueError):
        # Corrupted session file
        return None
    except Exception:
        return None


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

    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"):
        try:
            data = json.loads(f.read_text())
            info = SessionInfo.from_dict(data)
            if not info.is_expired:
                sessions.append(info)
        except Exception:
            continue
    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())
            info = SessionInfo.from_dict(data)
            if info.is_expired:
                f.unlink()
                removed += 1
        except Exception:
            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 (cross-terminal sharing)
    other_session = has_any_active_session(platform_info)
    if other_session is not None:
        # Adopt session for this device
        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(timezone.utc),
            expires_at=other_session.expires_at,
            device_id=generate_device_id(),
            ip_address=other_session.ip_address,
            metadata=other_session.metadata,
        )
        save_session(adopted, platform_info)
        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).
    """
    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(timezone.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 e:
        return False, f"Failed to invalidate session: {e}"


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: {result.session.username})"

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

    # Valid session
    session = result.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 60 seconds
        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:
                pass

        # 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
        if self._cached_session and not self._should_recheck():
            if not self._cached_session.is_expired:
                return SessionValidationResult(
                    status=SESSION_VALID,
                    session=self._cached_session,
                    message=f"Welcome back, {self._cached_session.username}!",
                    needs_login=False,
                )

        # 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 all 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(timezone.utc)
            # Save periodically (every 5 minutes of activity)
            if self._should_recheck():
                self.refresh()
