import logging
import hashlib
import logging
import time
from dataclasses import asdict, dataclass, field
from typing import Any, Dict, List, Optional, Union
from pathlib import Path

from config import paths
from config.v2_sessions import SessionsStore
from config.v2_settings import SettingsStore, ChannelSettings, RoutingSettings


logger = logging.getLogger(__name__)


DEFAULT_SHOW_MESSAGE_TYPES: List[str] = []


@dataclass
class ChannelRouting:
    """Per-channel agent routing configuration."""

    agent_backend: Optional[str] = None  # "claude" | "codex" | "opencode" | None
    opencode_agent: Optional[str] = None  # "build" | "plan" | ... | None
    opencode_model: Optional[str] = None  # "provider/model" | None
    opencode_reasoning_effort: Optional[str] = None  # "low" | "medium" | "high" | "xhigh" | None

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization"""
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict) -> "ChannelRouting":
        """Create from dictionary"""
        if data is None:
            return None
        return cls(
            agent_backend=data.get("agent_backend"),
            opencode_agent=data.get("opencode_agent"),
            opencode_model=data.get("opencode_model"),
            opencode_reasoning_effort=data.get("opencode_reasoning_effort"),
        )


@dataclass
class UserSettings:
    show_message_types: List[str] = field(
        default_factory=lambda: DEFAULT_SHOW_MESSAGE_TYPES.copy()
    )
    custom_cwd: Optional[str] = None
    channel_routing: Optional[ChannelRouting] = None
    enabled: bool = True

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization"""
        result = {
            "show_message_types": self.show_message_types,
            "custom_cwd": self.custom_cwd,
        }
        if self.channel_routing is not None:
            result["routing"] = self.channel_routing.to_dict()
        return result

    @classmethod
    def from_dict(cls, data: dict) -> "UserSettings":
        """Create from dictionary"""
        if data is None:
            return cls()
        payload = dict(data)
        routing_data = payload.pop("routing", None)
        show_message_types = payload.get("show_message_types")
        settings = cls(
            show_message_types=(
                show_message_types
                if show_message_types is not None
                else DEFAULT_SHOW_MESSAGE_TYPES.copy()
            ),
            custom_cwd=payload.get("custom_cwd"),
        )
        if routing_data is not None:
            settings.channel_routing = ChannelRouting.from_dict(routing_data)
        return settings


class SettingsManager:
    """Manages user personalization settings with JSON persistence"""

    MESSAGE_TYPE_ALIASES = {
        "tool_call": "toolcall",
        "tool": "toolcall",
    }

    def __init__(self, settings_file: Optional[str] = None):
        paths.ensure_data_dirs()
        self.settings_file = Path(settings_file) if settings_file else paths.get_settings_path()
        self._settings_mtime_ns: Optional[int] = None
        self._settings_fingerprint: Optional[str] = None
        self.settings: Dict[Union[int, str], UserSettings] = {}
        self.store = SettingsStore(self.settings_file)
        self.sessions_store = SessionsStore()
        self.sessions_store.load()
        self._load_settings()

    # ---------------------------------------------
    # Internal helpers
    # ---------------------------------------------
    def _normalize_user_id(self, user_id: Union[int, str]) -> str:
        """Normalize user_id consistently to string.

        Rationale: JSON object keys are strings; Slack IDs are strings; unifying to
        string avoids mixed-type keys (e.g., 123 vs "123").
        """
        return str(user_id)

    def _from_channel_settings(self, channel_settings: ChannelSettings) -> UserSettings:
        routing = ChannelRouting(
            agent_backend=channel_settings.routing.agent_backend,
            opencode_agent=channel_settings.routing.opencode_agent,
            opencode_model=channel_settings.routing.opencode_model,
            opencode_reasoning_effort=channel_settings.routing.opencode_reasoning_effort,
        )
        return UserSettings(
            show_message_types=self._normalize_show_message_types(
                channel_settings.show_message_types
            ),
            custom_cwd=channel_settings.custom_cwd,
            channel_routing=routing,
            enabled=channel_settings.enabled,
        )

    def _to_channel_settings(self, settings: UserSettings) -> ChannelSettings:
        routing = settings.channel_routing or ChannelRouting()
        return ChannelSettings(
            enabled=settings.enabled,
            show_message_types=self._normalize_show_message_types(
                settings.show_message_types
            ),
            custom_cwd=settings.custom_cwd,
            routing=RoutingSettings(
                agent_backend=routing.agent_backend,
                opencode_agent=routing.opencode_agent,
                opencode_model=routing.opencode_model,
                opencode_reasoning_effort=routing.opencode_reasoning_effort,
            ),
        )

    def _load_settings(self):
        """Load settings from JSON file"""
        self.store = SettingsStore(self.settings_file)
        self.settings = {}

        if not self.store.settings.channels:
            logger.info("No settings file found, starting with empty settings")
            return

        for channel_id, channel_settings in self.store.settings.channels.items():
            self.settings[str(channel_id)] = self._from_channel_settings(channel_settings)

        try:
            self._settings_mtime_ns = self.settings_file.stat().st_mtime_ns
            self._settings_fingerprint = self._compute_settings_fingerprint()
        except FileNotFoundError:
            self._settings_mtime_ns = None
            self._settings_fingerprint = None

        logger.info(f"Loaded settings for {len(self.settings)} channels")


    def _compute_settings_fingerprint(self) -> Optional[str]:
        try:
            data = self.settings_file.read_bytes()
        except FileNotFoundError:
            return None
        return hashlib.sha256(data).hexdigest()

    def _reload_if_changed(self) -> None:
        if not self.settings_file.exists():
            return
        try:
            mtime_ns = self.settings_file.stat().st_mtime_ns
        except FileNotFoundError:
            return
        fingerprint = None
        if self._settings_mtime_ns is None or mtime_ns != self._settings_mtime_ns:
            fingerprint = self._compute_settings_fingerprint()
        elif self._settings_fingerprint is None:
            fingerprint = self._compute_settings_fingerprint()
        if fingerprint and fingerprint != self._settings_fingerprint:
            logger.info("Settings file changed on disk, reloading")
            self._load_settings()
        elif fingerprint:
            self._settings_fingerprint = fingerprint
            self._settings_mtime_ns = mtime_ns
        else:
            self._settings_mtime_ns = mtime_ns

    def _save_settings(self):
        """Save settings to JSON file"""
        try:
            channels: Dict[str, ChannelSettings] = {}
            for settings_key, settings in self.settings.items():
                existing = self.store.settings.channels.get(str(settings_key))
                channel_settings = self._to_channel_settings(settings)
                if existing is not None:
                    channel_settings.enabled = existing.enabled
                channels[str(settings_key)] = channel_settings
            self.store.settings.channels = channels
            self.store.save()
            try:
                self._settings_mtime_ns = self.settings_file.stat().st_mtime_ns
                self._settings_fingerprint = self._compute_settings_fingerprint()
            except FileNotFoundError:
                self._settings_mtime_ns = None
                self._settings_fingerprint = None
            logger.info("Settings saved successfully")
        except Exception as e:
            logger.error(f"Error saving settings: {e}")

    def get_user_settings(self, user_id: Union[int, str]) -> UserSettings:
        """Get settings for a specific user"""
        normalized_id = self._normalize_user_id(user_id)

        self._reload_if_changed()

        # Return existing or create new
        if normalized_id not in self.settings:
            settings = UserSettings()
            if normalized_id in self.store.settings.channels:
                settings = self._from_channel_settings(
                    self.store.settings.channels[normalized_id]
                )
            self.settings[normalized_id] = settings
            self._save_settings()
        return self.settings[normalized_id]

    def update_user_settings(self, user_id: Union[int, str], settings: UserSettings):
        """Update settings for a specific user"""
        normalized_id = self._normalize_user_id(user_id)

        settings.show_message_types = self._normalize_show_message_types(
            settings.show_message_types
        )

        self.settings[normalized_id] = settings
        self._save_settings()

    def toggle_show_message_type(
        self, user_id: Union[int, str], message_type: str
    ) -> bool:
        """Toggle a message type in show list, returns new state (True if now shown)"""
        message_type = self._canonicalize_message_type(message_type)
        settings = self.get_user_settings(user_id)

        if message_type in settings.show_message_types:
            settings.show_message_types.remove(message_type)
            is_shown = False
        else:
            settings.show_message_types.append(message_type)
            is_shown = True

        self.update_user_settings(user_id, settings)
        return is_shown

    def set_custom_cwd(self, user_id: Union[int, str], cwd: str):
        """Set custom working directory for user"""
        settings = self.get_user_settings(user_id)
        settings.custom_cwd = cwd
        self.update_user_settings(user_id, settings)

    def get_custom_cwd(self, user_id: Union[int, str]) -> Optional[str]:
        """Get custom working directory for user"""
        settings = self.get_user_settings(user_id)
        return settings.custom_cwd

    def get_channel_settings(self, channel_id: Union[int, str]) -> Optional[ChannelSettings]:
        """Get raw ChannelSettings for a channel without creating defaults."""
        self._reload_if_changed()
        key = str(channel_id)
        return self.store.settings.channels.get(key)

    def is_message_type_hidden(
        self, user_id: Union[int, str], message_type: str
    ) -> bool:
        """Check if a message type is hidden for user (not in show_message_types)"""
        self._reload_if_changed()
        message_type = self._canonicalize_message_type(message_type)
        settings = self.get_user_settings(user_id)
        return message_type not in settings.show_message_types

    def save_user_settings(self, user_id: Union[int, str], settings: UserSettings):
        """Save settings for a specific user (alias for update_user_settings)"""
        self.update_user_settings(user_id, settings)

    def get_available_message_types(self) -> List[str]:
        """Get list of available message types that can be hidden"""
        return ["system", "assistant", "toolcall"]

    def get_message_type_display_names(self) -> Dict[str, str]:
        """Get display names for message types"""
        return {
            "system": "System",
            "assistant": "Assistant",
            "toolcall": "Toolcall",
        }

    def _ensure_agent_namespace(self, user_id: Union[int, str], agent_name: str) -> Dict[str, str]:
        user_key = self._normalize_user_id(user_id)
        return self.sessions_store.get_agent_map(user_key, agent_name)

    def set_agent_session_mapping(
        self,
        user_id: Union[int, str],
        agent_name: str,
        thread_id: str,
        session_id: str,
    ):
        """Store mapping between thread ID and agent session ID"""
        agent_map = self._ensure_agent_namespace(user_id, agent_name)
        agent_map[thread_id] = session_id
        self.sessions_store.save()
        logger.info(
            f"Stored {agent_name} session mapping for {user_id}: "
            f"{thread_id} -> {session_id}"
        )

    def get_agent_session_id(
        self,
        user_id: Union[int, str],
        thread_id: str,
        agent_name: str,
    ) -> Optional[str]:
        """Get agent session ID for given thread ID"""
        user_key = self._normalize_user_id(user_id)
        agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
        return agent_map.get(thread_id)

    def _canonicalize_message_type(self, message_type: str) -> str:
        """Normalize message type to canonical form to support aliases."""
        return self.MESSAGE_TYPE_ALIASES.get(message_type, message_type)

    def _normalize_show_message_types(self, show_message_types: Optional[List[str]]) -> List[str]:
        """Normalize and migrate show message types to current canonical schema."""
        allowed = {"system", "assistant", "toolcall"}
        if show_message_types is None:
            return DEFAULT_SHOW_MESSAGE_TYPES.copy()
        normalized: List[str] = []
        seen = set()

        for msg_type in show_message_types or []:
            canonical = self._canonicalize_message_type(msg_type)
            if canonical not in allowed:
                continue
            if canonical in seen:
                continue
            seen.add(canonical)
            normalized.append(canonical)

        return normalized

    def clear_agent_session_mapping(
        self,
        user_id: Union[int, str],
        agent_name: str,
        thread_id: str,
    ):
        """Clear session mapping for given thread ID"""
        user_key = self._normalize_user_id(user_id)
        agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
        if thread_id in agent_map:
            del agent_map[thread_id]
            logger.info(
                f"Cleared {agent_name} session mapping for user {user_id}: {thread_id}"
            )
            self.sessions_store.save()

    def clear_agent_sessions(self, user_id: Union[int, str], agent_name: str):
        """Clear every session mapping for the specified agent."""
        user_key = self._normalize_user_id(user_id)
        agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
        if agent_map:
            self.sessions_store.state.session_mappings[user_key][agent_name] = {}
            logger.info(
                f"Cleared all {agent_name} session namespaces for user {user_id}"
            )
            self.sessions_store.save()

    def clear_all_session_mappings(self, user_id: Union[int, str]):
        """Clear all session mappings for a user across agents"""
        user_key = self._normalize_user_id(user_id)
        agent_maps = self.sessions_store.state.session_mappings.get(user_key, {})
        if agent_maps:
            count = sum(len(agent_map) for agent_map in agent_maps.values())
            self.sessions_store.state.session_mappings[user_key] = {}
            logger.info(
                f"Cleared all session mappings ({count} bases) for user {user_id}"
            )
            self.sessions_store.save()

    def list_agent_sessions(
        self, user_id: Union[int, str], agent_name: str
    ) -> Dict[str, str]:
        """Get copy of session mappings (thread_id -> session_id) for an agent."""
        user_key = self._normalize_user_id(user_id)
        agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
        return dict(agent_map)

    # Backwards-compatible helpers for Claude-specific call sites
    def set_session_mapping(
        self,
        user_id: Union[int, str],
        thread_id: str,
        claude_session_id: str,
    ):
        self.set_agent_session_mapping(
            user_id, "claude", thread_id, claude_session_id
        )

    def get_claude_session_id(
        self, user_id: Union[int, str], thread_id: str
    ) -> Optional[str]:
        return self.get_agent_session_id(
            user_id, thread_id, agent_name="claude"
        )

    def clear_session_mapping(
        self,
        user_id: Union[int, str],
        thread_id: str,
    ):
        self.clear_agent_session_mapping(
            user_id, "claude", thread_id
        )

    # ---------------------------------------------
    # Slack thread management
    # ---------------------------------------------
    def mark_thread_active(
        self, user_id: Union[int, str], channel_id: str, thread_ts: str
    ):
        """Mark a Slack thread as active with current timestamp"""
        user_key = self._normalize_user_id(user_id)
        channel_map = self.sessions_store.get_thread_map(user_key, channel_id)
        channel_map[thread_ts] = time.time()
        self.sessions_store.save()
        logger.info(
            f"Marked thread active for user {user_id}: channel={channel_id}, thread={thread_ts}"
        )

    def is_thread_active(
        self, user_id: Union[int, str], channel_id: str, thread_ts: str
    ) -> bool:
        """Check if a Slack thread is active (within 24 hours)"""
        user_key = self._normalize_user_id(user_id)

        # First cleanup expired threads for this channel
        self._cleanup_expired_threads_for_channel(user_id, channel_id)

        channel_map = self.sessions_store.get_thread_map(user_key, channel_id)
        return thread_ts in channel_map

    def _cleanup_expired_threads_for_channel(
        self, user_id: Union[int, str], channel_id: str
    ):
        """Remove threads older than 24 hours for a specific channel"""
        user_key = self._normalize_user_id(user_id)
        channel_map = self.sessions_store.get_thread_map(user_key, channel_id)

        if not channel_map:
            return

        current_time = time.time()
        twenty_four_hours_ago = current_time - (24 * 60 * 60)

        expired_threads = [
            thread_ts
            for thread_ts, last_active in channel_map.items()
            if last_active < twenty_four_hours_ago
        ]

        if expired_threads:
            for thread_ts in expired_threads:
                del channel_map[thread_ts]

            if not channel_map:
                self.sessions_store.state.active_slack_threads[user_key].pop(channel_id, None)

            self.sessions_store.save()
            logger.info(
                f"Cleaned up {len(expired_threads)} expired threads for channel {channel_id}"
            )

    def cleanup_all_expired_threads(self, user_id: Union[int, str]):
        """Remove all threads older than 24 hours for all channels"""
        user_key = self._normalize_user_id(user_id)
        channel_map = self.sessions_store.state.active_slack_threads.get(user_key, {})

        if not channel_map:
            return

        channels_to_clean = list(channel_map.keys())
        for channel_id in channels_to_clean:
            self._cleanup_expired_threads_for_channel(user_id, channel_id)

    # ---------------------------------------------
    # Channel routing management
    # ---------------------------------------------
    def get_channel_routing(
        self, settings_key: Union[int, str]
    ) -> Optional[ChannelRouting]:
        """Get channel routing override for the given settings key."""
        self._reload_if_changed()
        settings = self.get_user_settings(settings_key)
        return settings.channel_routing

    def set_channel_routing(
        self, settings_key: Union[int, str], routing: ChannelRouting
    ):
        """Set channel routing override."""
        settings = self.get_user_settings(settings_key)
        settings.channel_routing = routing
        self.update_user_settings(settings_key, settings)
        logger.info(
            f"Updated channel routing for {settings_key}: "
            f"backend={routing.agent_backend}, "
            f"opencode_agent={routing.opencode_agent}, "
            f"opencode_model={routing.opencode_model}"
        )

    def clear_channel_routing(self, settings_key: Union[int, str]):
        """Clear channel routing override (fall back to default backend)."""
        settings = self.get_user_settings(settings_key)
        if settings.channel_routing:
            settings.channel_routing = None
            self.update_user_settings(settings_key, settings)
            logger.info(f"Cleared channel routing for {settings_key}")
