# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Session / Auth Skill

Authenticate users, create/validate/revoke sessions.
Wraps core/users.py session management with JSON fallback.

Dependencies: None (pure stdlib)
"""

import hashlib
import hmac
import json
import logging
import secrets
from datetime import datetime, timedelta
from pathlib import Path


def _get_data_dir():
    """Get data directory (tenant-scoped in Reflection, default in standalone)."""
    try:
        from familiar.core import paths

        return paths.DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


try:
    from familiar.core.users import get_user_manager

    CORE_USERS_AVAILABLE = True
except ImportError:
    CORE_USERS_AVAILABLE = False

logger = logging.getLogger(__name__)

DATA_DIR = _get_data_dir()
SESSIONS_FILE = DATA_DIR / "sessions.json"
USERS_FILE = DATA_DIR / "users.json"

DEFAULT_SESSION_HOURS = 24


def _load_json(filepath):
    if filepath.exists():
        try:
            with open(filepath, encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError):
            pass
    return {}


def _save_json(filepath, data):
    filepath.parent.mkdir(parents=True, exist_ok=True)
    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)


def _hash_token(token):
    return hashlib.sha256(token.encode()).hexdigest()


def _verify_password(password, stored_hash):
    try:
        salt_hex, key_hex = stored_hash.split(":", 1)
        salt = bytes.fromhex(salt_hex)
        expected = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 600000)
        return hmac.compare_digest(expected.hex(), key_hex)
    except (ValueError, TypeError):
        return False


def _audit_event(action, user_id="", detail="", severity="info"):
    """Log auth event to audit trail."""
    try:
        import importlib.util
        import sys

        audit_path = Path(__file__).parent.parent / "audit" / "skill.py"
        spec = importlib.util.spec_from_file_location("audit_sess", str(audit_path))
        module = importlib.util.module_from_spec(spec)
        if "audit_sess" not in sys.modules:
            sys.modules["audit_sess"] = module
            spec.loader.exec_module(module)
        else:
            module = sys.modules["audit_sess"]
        module.log_event(
            {
                "action": action,
                "user_id": user_id,
                "detail": detail,
                "severity": severity,
                "resource": "session",
            }
        )
    except Exception:
        logger.debug(f"Audit log unavailable: {action} {user_id}")


# === Tool Handlers ===


def authenticate(data):
    """Authenticate a user by email + password and create a session."""
    email = data.get("email", "").strip()
    password = data.get("password", "").strip()

    if not email or not password:
        return "Please provide email and password."

    if CORE_USERS_AVAILABLE:
        try:
            mgr = get_user_manager()
            user = mgr.get_user_by_email(email)
            if not user:
                _audit_event("login_failed", email, "User not found", "warning")
                return "❌ Authentication failed: invalid credentials."
            if not user.is_active:
                _audit_event("login_failed", user.id, "Account deactivated", "warning")
                return "❌ Authentication failed: account deactivated."
            # Core creates session
            session = mgr.create_session(user.id)
            _audit_event("login", user.id, f"Session created: {session.id[:8]}...")
            return (
                f"✅ Authenticated: {user.name} ({user.email})\n"
                f"   Session token: {session.id}\n"
                f"   Expires: {session.expires_at}"
            )
        except Exception as e:
            logger.warning(f"Core auth failed: {e}")

    # JSON fallback
    users_data = _load_json(USERS_FILE)
    user = None
    for u in users_data.get("users", []):
        if u.get("email", "").lower() == email.lower():
            user = u
            break

    if not user:
        _audit_event("login_failed", email, "User not found", "warning")
        return "❌ Authentication failed: invalid credentials."

    if user.get("status") == "deactivated":
        _audit_event("login_failed", user["id"], "Account deactivated", "warning")
        return "❌ Authentication failed: account deactivated."

    if not user.get("password_hash"):
        return "❌ No password set for this user. Use update_user to set one."

    if not _verify_password(password, user["password_hash"]):
        _audit_event("login_failed", user["id"], "Wrong password", "warning")
        return "❌ Authentication failed: invalid credentials."

    # Create session
    token = secrets.token_urlsafe(32)
    expires_hours = int(data.get("session_hours", DEFAULT_SESSION_HOURS))
    session = {
        "token_hash": _hash_token(token),
        "user_id": user["id"],
        "user_name": user["name"],
        "user_email": user["email"],
        "user_role": user.get("role", "staff"),
        "created_at": datetime.now().isoformat(),
        "expires_at": (datetime.now() + timedelta(hours=expires_hours)).isoformat(),
        "ip_address": data.get("ip_address", ""),
    }

    sessions_data = _load_json(SESSIONS_FILE)
    sessions_data.setdefault("sessions", []).append(session)
    # Prune expired
    now = datetime.now().isoformat()
    sessions_data["sessions"] = [s for s in sessions_data["sessions"] if s["expires_at"] > now]
    _save_json(SESSIONS_FILE, sessions_data)

    # Update last active
    user["last_active"] = datetime.now().isoformat()
    _save_json(USERS_FILE, users_data)

    _audit_event("login", user["id"], f"Session created ({expires_hours}h)")

    return (
        f"✅ Authenticated: {user['name']} ({user['email']}) [{user.get('role', 'staff')}]\n"
        f"   Session token: {token}\n"
        f"   Expires: {session['expires_at'][:19]}"
    )


def create_session(data):
    """Create a session for a user (admin/system use, no password check)."""
    user_id = data.get("user_id", "").strip()
    if not user_id:
        return "Please provide user_id."

    expires_hours = int(data.get("session_hours", DEFAULT_SESSION_HOURS))

    if CORE_USERS_AVAILABLE:
        try:
            mgr = get_user_manager()
            user = mgr.get_user(user_id)
            if not user:
                return f"User not found: {user_id}"
            session = mgr.create_session(user.id)
            _audit_event("session_created", user.id, "Admin session creation")
            return (
                f"✅ Session created for {user.name}\n"
                f"   Token: {session.id}\n"
                f"   Expires: {session.expires_at}"
            )
        except Exception as e:
            logger.warning(f"Core session creation failed: {e}")

    # JSON fallback
    users_data = _load_json(USERS_FILE)
    user = None
    for u in users_data.get("users", []):
        if u["id"] == user_id:
            user = u
            break
    if not user:
        return f"User not found: {user_id}"

    token = secrets.token_urlsafe(32)
    session = {
        "token_hash": _hash_token(token),
        "user_id": user["id"],
        "user_name": user["name"],
        "user_email": user.get("email", ""),
        "user_role": user.get("role", "staff"),
        "created_at": datetime.now().isoformat(),
        "expires_at": (datetime.now() + timedelta(hours=expires_hours)).isoformat(),
    }

    sessions_data = _load_json(SESSIONS_FILE)
    sessions_data.setdefault("sessions", []).append(session)
    _save_json(SESSIONS_FILE, sessions_data)

    _audit_event("session_created", user_id, f"Admin-created ({expires_hours}h)")

    return (
        f"✅ Session created for {user['name']}\n"
        f"   Token: {token}\n"
        f"   Expires: {session['expires_at'][:19]}"
    )


def validate_session(data):
    """Validate a session token. Returns user info if valid."""
    token = data.get("token", "").strip()
    if not token:
        return "Please provide a session token."

    if CORE_USERS_AVAILABLE:
        try:
            mgr = get_user_manager()
            user = mgr.authenticate_session(token)
            if user:
                return (
                    f"✅ Session valid: {user.name} ({user.email}) [{user.role.value}]\n"
                    f"   User ID: {user.id}"
                )
            return "❌ Session invalid or expired."
        except Exception as e:
            logger.warning(f"Core session validation failed: {e}")

    # JSON fallback
    token_hash = _hash_token(token)
    sessions_data = _load_json(SESSIONS_FILE)
    now = datetime.now().isoformat()

    for s in sessions_data.get("sessions", []):
        if s["token_hash"] == token_hash:
            if s["expires_at"] > now:
                return (
                    f"✅ Session valid: {s['user_name']} ({s['user_email']}) [{s.get('user_role', 'staff')}]\n"
                    f"   User ID: {s['user_id']}\n"
                    f"   Expires: {s['expires_at'][:19]}"
                )
            else:
                return "❌ Session expired."

    return "❌ Session invalid or expired."


def revoke_session(data):
    """Revoke a specific session or all sessions for a user."""
    token = data.get("token", "").strip()
    user_id = data.get("user_id", "").strip()
    revoke_all = data.get("revoke_all", False)

    if not token and not user_id:
        return "Please provide token or user_id."

    if not data.get("_confirmed"):
        from familiar.core.confirmations import needs_confirmation

        token_or_user = data.get("token", "") or data.get("user", "all")
        return needs_confirmation(
            tool_name="revoke_session",
            tool_input=data,
            preview=f"Revoke session(s) for '{token_or_user}'. Users will need to re-authenticate.",
            risk="high",
        )

    if CORE_USERS_AVAILABLE:
        try:
            mgr = get_user_manager()
            if revoke_all and user_id:
                mgr.invalidate_all_sessions(user_id)
                _audit_event("sessions_revoked_all", user_id, "All sessions revoked")
                return f"✅ All sessions revoked for user {user_id}"
            elif token:
                mgr.invalidate_session(token)
                _audit_event("session_revoked", "", "Token revoked")
                return "✅ Session revoked."
        except Exception as e:
            logger.warning(f"Core session revocation failed: {e}")

    # JSON fallback
    sessions_data = _load_json(SESSIONS_FILE)
    original_count = len(sessions_data.get("sessions", []))

    if revoke_all and user_id:
        sessions_data["sessions"] = [
            s for s in sessions_data.get("sessions", []) if s["user_id"] != user_id
        ]
        revoked = original_count - len(sessions_data["sessions"])
        _save_json(SESSIONS_FILE, sessions_data)
        _audit_event("sessions_revoked_all", user_id, f"{revoked} sessions revoked")
        return f"✅ Revoked {revoked} session(s) for user {user_id}"

    if token:
        token_hash = _hash_token(token)
        sessions_data["sessions"] = [
            s for s in sessions_data.get("sessions", []) if s["token_hash"] != token_hash
        ]
        revoked = original_count - len(sessions_data["sessions"])
        _save_json(SESSIONS_FILE, sessions_data)
        if revoked:
            _audit_event("session_revoked", "", "Token revoked")
            return "✅ Session revoked."
        return "Session not found."

    return "No action taken."


# === Tool Definitions ===

TOOLS = [
    {
        "name": "authenticate",
        "description": (
            "Authenticate a user with email and password, create a session, and return "
            "a bearer token. Use this when a user needs to log in to the system. "
            "Credentials are verified against core user management or the local JSON "
            "fallback, and an audit event is recorded for each attempt."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "email": {"type": "string", "description": "User email"},
                "password": {"type": "string", "description": "User password"},
                "session_hours": {
                    "type": "integer",
                    "default": 24,
                    "description": "Session duration in hours",
                },
                "ip_address": {"type": "string", "description": "Client IP for audit"},
            },
            "required": ["email", "password"],
        },
        "handler": authenticate,
        "category": "sessions",
    },
    {
        "name": "create_session",
        "description": (
            "Create a session token for a user without requiring a password, intended "
            "for admin or system use. Use this for service-to-service authentication "
            "or when an administrator needs to grant access on behalf of a user. "
            "An audit event is recorded for accountability."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "user_id": {"type": "string", "description": "User ID"},
                "session_hours": {"type": "integer", "default": 24},
            },
            "required": ["user_id"],
        },
        "handler": create_session,
        "category": "sessions",
    },
    {
        "name": "validate_session",
        "description": (
            "Validate a session token and return the associated user information and "
            "role if the session is still active. Use this to verify authentication "
            "status before granting access to protected resources. Returns an error "
            "message if the token is expired or invalid."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "token": {"type": "string", "description": "Session token"},
            },
            "required": ["token"],
        },
        "handler": validate_session,
        "category": "sessions",
    },
    {
        "name": "revoke_session",
        "description": (
            "Revoke a specific session token or all sessions for a given user, forcing "
            "re-authentication. Use this to log out a user, invalidate a compromised "
            "token, or perform a security sweep. Set revoke_all=true with a user_id "
            "to invalidate every active session for that user."
        ),
        "requires_confirmation": True,
        "input_schema": {
            "type": "object",
            "properties": {
                "token": {"type": "string", "description": "Session token to revoke"},
                "user_id": {"type": "string", "description": "User ID (for revoking all sessions)"},
                "revoke_all": {
                    "type": "boolean",
                    "default": False,
                    "description": "Revoke ALL sessions for user",
                },
            },
        },
        "handler": revoke_session,
        "category": "sessions",
    },
]
