# 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

"""
Channel Authentication Module - Phase 1

Provides unified authentication for all channels (Telegram, Discord, CLI, etc.).
Integrates with UserManager for multi-user support.

Authentication Flow:
1. User sends message via channel
2. Channel looks up user by channel ID (telegram_id, discord_id, etc.)
3. If found → Create UserContext and proceed
4. If not found → Prompt to link account or operate as guest

Linking Flow:
1. User sends /link email@org.org
2. System sends magic link email
3. User clicks link or enters code
4. Channel ID linked to user account
"""

from __future__ import annotations

import hashlib
import logging
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Any, Optional, Tuple

logger = logging.getLogger(__name__)

# Import user management
try:
    from ..core.data_store import get_data_store
    from ..core.users import User, UserContext, UserManager, UserRole, UserStatus, get_user_manager

    HAS_USER_MANAGEMENT = True
except ImportError:
    logger.warning("User management not available - running in legacy mode")
    HAS_USER_MANAGEMENT = False
    UserManager = None
    User = None
    UserRole = None
    UserStatus = None
    UserContext = None
    get_user_manager = None
    get_data_store = None


class ChannelType(str, Enum):
    """Supported channel types."""

    TELEGRAM = "telegram"
    DISCORD = "discord"
    SIGNAL = "signal"
    IMESSAGE = "imessage"
    WHATSAPP = "whatsapp"
    CLI = "cli"
    WEB = "web"
    MATRIX = "matrix"
    TEAMS = "teams"
    PWA = "pwa"
    GITHUB = "github"
    SLACK = "slack"


class AuthMode(str, Enum):
    """Authentication modes."""

    MULTI_USER = "multi_user"  # Full UserManager integration
    WHITELIST = "whitelist"  # Legacy allowed_users list
    OPEN = "open"  # No authentication (dev only)


@dataclass
class ChannelUser:
    """
    Authenticated user for a channel.
    Combines User model with channel-specific info.
    """

    user: Optional[User]  # None if guest/unlinked
    channel_type: ChannelType
    channel_id: str  # Telegram ID, Discord ID, etc.
    is_linked: bool  # Whether channel is linked to account
    is_guest: bool  # Operating as guest
    display_name: str  # Name to show in responses

    @property
    def user_id(self) -> Optional[str]:
        return self.user.id if self.user else None

    @property
    def role(self) -> Optional[Any]:
        return self.user.role if self.user else None

    @property
    def is_admin(self) -> bool:
        if not self.user or not UserRole:
            return False
        return self.user.role == UserRole.ADMIN

    @property
    def can_write(self) -> bool:
        if not self.user:
            return False  # Guests can't write
        if hasattr(self.user.role, "can_write"):
            return self.user.role.can_write
        return True  # Default allow if role system not available

    def to_context(self, session_token: str = None) -> Optional[Any]:
        """Convert to UserContext for passing to agent/skills."""
        if not self.user or not UserContext:
            return None
        return UserContext(
            user=self.user,
            session_token=session_token or "",
        )


class ChannelAuthenticator:
    """
    Unified authentication for all channels.

    Usage:
        auth = ChannelAuthenticator(mode=AuthMode.MULTI_USER)

        # In message handler:
        channel_user = auth.authenticate(
            channel_type=ChannelType.TELEGRAM,
            channel_id=str(update.effective_user.id),
            display_name=update.effective_user.first_name
        )

        if not channel_user.is_linked:
            # Prompt to link account
            pass
    """

    def __init__(
        self,
        mode: AuthMode = AuthMode.MULTI_USER,
        allowed_ids: list = None,
        allow_guests: bool = True,
    ):
        """
        Initialize authenticator.

        Args:
            mode: Authentication mode
            allowed_ids: For WHITELIST mode, list of allowed channel IDs
            allow_guests: Whether to allow unlinked users as guests
        """
        self.mode = mode
        self.allowed_ids = set(str(id) for id in (allowed_ids or []))
        self.allow_guests = allow_guests

        # Pending link requests: channel_id -> (email, token, expires)
        self._pending_links: dict = {}

        # Get user manager if available
        self._user_manager = None
        if mode == AuthMode.MULTI_USER and UserManager:
            try:
                self._user_manager = get_user_manager()
            except Exception as e:
                logger.warning(f"Could not initialize UserManager: {e}")

    def authenticate(
        self, channel_type: ChannelType, channel_id: str, display_name: str = None
    ) -> ChannelUser:
        """
        Authenticate a user from a channel.

        Args:
            channel_type: The channel type (telegram, discord, etc.)
            channel_id: The user's ID on that channel
            display_name: Display name from the channel

        Returns:
            ChannelUser with authentication status
        """
        channel_id = str(channel_id)

        # Mode: OPEN - allow everyone as guest
        if self.mode == AuthMode.OPEN:
            return ChannelUser(
                user=None,
                channel_type=channel_type,
                channel_id=channel_id,
                is_linked=False,
                is_guest=True,
                display_name=display_name or f"Guest-{channel_id[:8]}",
            )

        # Mode: WHITELIST - check allowed list
        if self.mode == AuthMode.WHITELIST:
            if self.allowed_ids and channel_id not in self.allowed_ids:
                return None  # Not authorized
            return ChannelUser(
                user=None,
                channel_type=channel_type,
                channel_id=channel_id,
                is_linked=False,
                is_guest=True,
                display_name=display_name or f"User-{channel_id[:8]}",
            )

        # Mode: MULTI_USER - use UserManager
        if self.mode == AuthMode.MULTI_USER and self._user_manager:
            user = self._lookup_user(channel_type, channel_id)

            if user:
                # Found linked user
                self._user_manager.update_last_active(user.id)
                return ChannelUser(
                    user=user,
                    channel_type=channel_type,
                    channel_id=channel_id,
                    is_linked=True,
                    is_guest=False,
                    display_name=user.display_name,
                )
            elif self.allow_guests:
                # Allow as guest
                return ChannelUser(
                    user=None,
                    channel_type=channel_type,
                    channel_id=channel_id,
                    is_linked=False,
                    is_guest=True,
                    display_name=display_name or f"Guest-{channel_id[:8]}",
                )
            else:
                return None  # Not authorized

        # Fallback: allow as guest
        return ChannelUser(
            user=None,
            channel_type=channel_type,
            channel_id=channel_id,
            is_linked=False,
            is_guest=True,
            display_name=display_name or "Guest",
        )

    def _lookup_user(self, channel_type: ChannelType, channel_id: str) -> Optional[User]:
        """Look up a user by their channel ID."""
        if not self._user_manager:
            return None

        try:
            if channel_type == ChannelType.TELEGRAM:
                return self._user_manager.get_user_by_telegram(int(channel_id))
            elif channel_type == ChannelType.DISCORD:
                return self._user_manager.get_user_by_discord(int(channel_id))
            elif channel_type == ChannelType.MATRIX:
                return self._user_manager.get_user_by_matrix(channel_id)
        except (ValueError, TypeError):
            logger.warning(f"Invalid channel ID format: {channel_id}")

        return None

    # ---- Account Linking ----

    def start_link(
        self, channel_type: ChannelType, channel_id: str, email: str
    ) -> Tuple[bool, str]:
        """
        Start the account linking process.

        Args:
            channel_type: The channel type
            channel_id: The channel user ID
            email: Email to link to

        Returns:
            (success, message) tuple
        """
        if not self._user_manager:
            return False, "Multi-user mode not enabled"

        email = email.lower().strip()

        # Check if user exists
        user = self._user_manager.get_user_by_email(email)
        if not user:
            return False, f"No account found for {email}. Ask an admin to invite you."

        if not user.is_active:
            return False, "Account is not active. Contact an admin."

        # Check if already linked
        existing = self._lookup_user(channel_type, channel_id)
        if existing:
            return False, f"This {channel_type.value} account is already linked to {existing.email}"

        # Generate link code (6 digits for easy entry)
        code = "".join(secrets.choice("0123456789") for _ in range(6))
        token_hash = hashlib.sha256(code.encode()).hexdigest()
        expires = datetime.now(timezone.utc) + timedelta(minutes=10)

        # Store pending link
        key = f"{channel_type.value}:{channel_id}"
        self._pending_links[key] = {
            "email": email,
            "token_hash": token_hash,
            "expires": expires,
            "user_id": user.id,
        }

        # Send the verification code to the email owner — never to the requester.
        self._send_link_code_email(email, code, channel_type)
        logger.info(f"Link code sent to {email} for {channel_type.value} linking")

        return True, (
            f"A 6-digit verification code has been sent to {email}.\n"
            "Reply with /confirm <code> to link your account.\n"
            "The code expires in 10 minutes."
        )

    def _send_link_code_email(self, email: str, code: str, channel_type: ChannelType):
        """Send the account-linking verification code to the email owner."""
        try:
            from ..skills.email.skill import send_email

            send_email(
                {
                    "to": email,
                    "subject": "Familiar - Account Link Verification Code",
                    "body": (
                        f"Your verification code to link your {channel_type.value} "
                        f"account is: {code}\n\n"
                        f"Reply with /confirm {code} in {channel_type.value} to "
                        "complete the link.\n\n"
                        "This code expires in 10 minutes.\n\n"
                        "If you did not request this, you can ignore this message."
                    ),
                }
            )
        except Exception as e:
            logger.warning(f"Could not send link code email to {email}: {e}")
            # Code is still stored — admin can retrieve it from logs if email fails,
            # but it is never disclosed to the requesting channel user.

    def confirm_link(
        self, channel_type: ChannelType, channel_id: str, code: str
    ) -> Tuple[bool, str]:
        """
        Confirm account linking with verification code.

        Args:
            channel_type: The channel type
            channel_id: The channel user ID
            code: Verification code from email

        Returns:
            (success, message) tuple
        """
        if not self._user_manager:
            return False, "Multi-user mode not enabled"

        key = f"{channel_type.value}:{channel_id}"
        pending = self._pending_links.get(key)

        if not pending:
            return False, "No pending link request. Use /link email@example.org first."

        # Check expiration
        if datetime.now(timezone.utc) > pending["expires"]:
            del self._pending_links[key]
            return False, "Link code expired. Please try again."

        # Verify code
        token_hash = hashlib.sha256(code.strip().encode()).hexdigest()
        if token_hash != pending["token_hash"]:
            return False, "Invalid code. Please check and try again."

        # Link the account
        user_id = pending["user_id"]
        success = False

        if channel_type == ChannelType.TELEGRAM:
            success = self._user_manager.link_telegram(user_id, int(channel_id))
        elif channel_type == ChannelType.DISCORD:
            success = self._user_manager.link_discord(user_id, int(channel_id))
        elif channel_type == ChannelType.MATRIX:
            success = self._user_manager.link_matrix(user_id, channel_id)

        # Clean up
        del self._pending_links[key]

        if success:
            user = self._user_manager.get_user(user_id)
            return True, f"✅ Account linked! Welcome, {user.display_name}!"
        else:
            return False, "Failed to link account. It may already be linked to another user."

    def unlink(self, channel_type: ChannelType, channel_id: str) -> Tuple[bool, str]:
        """
        Unlink a channel from user account.

        Args:
            channel_type: The channel type
            channel_id: The channel user ID

        Returns:
            (success, message) tuple
        """
        if not self._user_manager:
            return False, "Multi-user mode not enabled"

        user = self._lookup_user(channel_type, channel_id)
        if not user:
            return False, "This account is not linked."

        # Clear the link
        if channel_type == ChannelType.TELEGRAM:
            self._user_manager.update_user(user.id, telegram_id=None)
        elif channel_type == ChannelType.DISCORD:
            self._user_manager.update_user(user.id, discord_id=None)
        elif channel_type == ChannelType.MATRIX:
            self._user_manager.update_user(user.id, matrix_id=None)

        return True, "Account unlinked. You can link again anytime with /link"


# ============================================================
# HELPER FUNCTIONS
# ============================================================


def create_authenticator(
    config=None, mode: AuthMode = None, allowed_ids: list = None
) -> ChannelAuthenticator:
    """
    Create a ChannelAuthenticator from config or explicit parameters.

    Args:
        config: Familiar Config object
        mode: Override auth mode
        allowed_ids: Override allowed IDs list

    Returns:
        Configured ChannelAuthenticator
    """
    # Determine mode
    if mode is None:
        if config and hasattr(config, "auth") and hasattr(config.auth, "mode"):
            mode = AuthMode(config.auth.mode)
        else:
            # Default to MULTI_USER if UserManager is available
            mode = AuthMode.MULTI_USER if UserManager else AuthMode.WHITELIST

    # Get allowed IDs from config
    if allowed_ids is None and config:
        allowed_ids = []
        if hasattr(config, "channels"):
            if hasattr(config.channels, "telegram_allowed_users"):
                allowed_ids.extend(config.channels.telegram_allowed_users or [])
            if hasattr(config.channels, "discord_allowed_users"):
                allowed_ids.extend(config.channels.discord_allowed_users or [])

    return ChannelAuthenticator(mode=mode, allowed_ids=allowed_ids, allow_guests=True)


def get_user_data_store(channel_user: ChannelUser):
    """
    Get a DataStore for an authenticated channel user.

    Args:
        channel_user: Authenticated ChannelUser

    Returns:
        DataStore if user is linked, None otherwise
    """
    if not channel_user.user:
        return None

    try:
        return get_data_store(user_id=channel_user.user.id, user_role=channel_user.user.role.value)
    except Exception as e:
        logger.warning(f"Could not get data store: {e}")
        return None


# ============================================================
# MESSAGE DECORATORS
# ============================================================


def require_linked(func):
    """
    Decorator to require a linked account for a command.

    Usage:
        @require_linked
        async def handle_sensitive_command(update, context, channel_user):
            # Only runs if channel_user.is_linked
            pass
    """

    async def wrapper(self, update, context, *args, **kwargs):
        channel_user = kwargs.get("channel_user") or getattr(context, "channel_user", None)

        if not channel_user or not channel_user.is_linked:
            await update.message.reply_text(
                "🔒 This command requires a linked account.\n\n"
                "Use /link your@email.org to link your account."
            )
            return

        return await func(self, update, context, *args, **kwargs)

    return wrapper


def require_role(min_role):
    """
    Decorator to require a minimum role level.

    Usage:
        @require_role(UserRole.ADMIN)
        async def handle_admin_command(update, context, channel_user):
            # Only runs if channel_user is admin
            pass
    """

    def decorator(func):
        async def wrapper(self, update, context, *args, **kwargs):
            channel_user = kwargs.get("channel_user") or getattr(context, "channel_user", None)

            if not channel_user or not channel_user.user:
                await update.message.reply_text(
                    "🔒 This command requires a linked account.\n\n"
                    "Use /link your@email.org to link your account."
                )
                return

            # Role hierarchy check only if UserRole is available
            if UserRole:
                role_order = [UserRole.READONLY, UserRole.STAFF, UserRole.ADMIN]
                user_level = (
                    role_order.index(channel_user.role) if channel_user.role in role_order else -1
                )
                min_level = role_order.index(min_role) if min_role in role_order else 999

                if user_level < min_level:
                    await update.message.reply_text(
                        f"🔒 This command requires {min_role.value} access.\n"
                        f"Your role: {channel_user.role.value}"
                    )
                    return

            return await func(self, update, context, *args, **kwargs)

        return wrapper

    return decorator
