"""
Startup display banner for Claude MPM.

Shows welcome message, version info, ASCII art, and what's new section.
"""

import json
import os
import re
import shutil
import subprocess  # nosec B404 - required for git operations
from pathlib import Path
from typing import List

from claude_mpm.core.logging_utils import get_logger
from claude_mpm.utils.git_analyzer import is_git_repository

logger = get_logger(__name__)

# ANSI color codes
CYAN = "\033[36m"  # Cyan for header highlight (Claude Code style)
DIM = "\033[2m"  # Dim text for subtle launch message
RESET = "\033[0m"

# Banner dimension defaults (will be calculated based on terminal width)
MIN_WIDTH = 100  # Minimum banner width
MAX_WIDTH = 200  # Maximum banner width
DEFAULT_WIDTH = 160  # Default if terminal width cannot be determined


def _get_terminal_width() -> int:
    """
    Get terminal width with reasonable bounds.

    Returns:
        Terminal width (75% of actual) clamped between MIN_WIDTH and MAX_WIDTH
    """
    try:
        full_width = shutil.get_terminal_size().columns
        # Use 75% of terminal width for more compact display
        width = int(full_width * 0.75)
        # Apply reasonable bounds
        return max(MIN_WIDTH, min(width, MAX_WIDTH))
    except Exception:
        # 75% of 160 = 120
        return 120


def _get_username() -> str:
    """Get username from environment or default to 'User'."""
    return os.environ.get("USER") or os.environ.get("USERNAME") or "User"


def _get_recent_commits(max_commits: int = 3) -> List[str]:
    """
    Get recent git commits for display in startup banner.

    Args:
        max_commits: Maximum number of commits to retrieve (default: 3)

    Returns:
        List of formatted commit strings (hash • relative_time • message)
        Empty list if not a git repo or if any error occurs

    Format: "a3f5b7c • 2 hours ago • fix: resolve critical error"
    """
    try:
        # Check if we're in a git repository
        if not is_git_repository("."):
            return []

        # Run git log with custom format (safe - no user input)
        result = subprocess.run(  # nosec B603 B607
            ["git", "log", "--format=%h • %ar • %s", f"-{max_commits}"],
            capture_output=True,
            text=True,
            check=True,
            timeout=2,
        )

        if not result.stdout.strip():
            return []

        # Split into lines and return
        commits = [
            line.strip() for line in result.stdout.strip().split("\n") if line.strip()
        ]
        return commits[:max_commits]

    except Exception:
        # Fail silently - return empty list on any error
        # (not a git repo, git not installed, timeout, etc.)
        return []


def _strip_ansi_codes(text: str) -> str:
    """Remove ANSI color codes from text."""
    ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
    return ansi_escape.sub("", text)


def _get_display_width(text: str) -> int:
    """
    Calculate display width accounting for emojis and wide characters.

    Args:
        text: Text to measure (without ANSI codes)

    Returns:
        Display width in terminal columns
    """
    width = 0
    for char in text:
        # Emoji and wide characters take 2 columns
        if ord(char) > 0x1F000:  # Emoji range
            width += 2
        else:
            width += 1
    return width


def _parse_changelog_highlights(max_items: int = 3, max_width: int = 130) -> List[str]:
    """
    Parse CHANGELOG.md to extract highlights from latest version.

    Args:
        max_items: Maximum number of highlights to return
        max_width: Maximum width for each highlight

    Returns:
        List of formatted bullet points for display.
    """
    changelog_path = Path(__file__).parent.parent.parent.parent / "CHANGELOG.md"

    if not changelog_path.exists():
        return ["No changelog available"]

    try:
        with open(changelog_path, encoding="utf-8") as f:
            lines = f.readlines()

        # Find latest version section (skip [Unreleased])
        in_version_section = False
        highlights = []

        for i, line in enumerate(lines):
            line = line.rstrip()

            # Skip unreleased section
            if line.startswith("## [Unreleased]"):
                continue

            # Found latest version
            if line.startswith("## [") and not in_version_section:
                in_version_section = True
                continue

            # Next version section means we're done
            if line.startswith("## [") and in_version_section:
                break

            # Extract any bullet points from any section in latest version
            if in_version_section:
                # Skip section headers
                if line.startswith("###"):
                    continue

                # Extract bullet points
                if line.startswith("-"):
                    # Get first line of multi-line items
                    item = line[2:].strip()  # Remove "- " prefix

                    # Clean up markdown formatting
                    item = item.replace("**", "")  # Remove bold markers

                    # Truncate if too long
                    if len(item) > max_width:
                        item = item[: max_width - 3] + "..."

                    highlights.append(item)

                    if len(highlights) >= max_items:
                        break

        if not highlights:
            highlights = ["See CHANGELOG.md for details"]

        return highlights[:max_items]

    except Exception:
        return ["See CHANGELOG.md for details"]


def _get_alien_art() -> List[str]:
    """Return multi-alien ASCII art with teal/cyan highlighting."""
    return [
        f"{CYAN}▐▛███▜▌ ▐▛███▜▌{RESET}",  # Two aliens - Width: 15 chars
        f"{CYAN}▝▜█████▛▘▝▜█████▛▘{RESET}",  # Two aliens base - Width: 18 chars
        f"{CYAN}▘▘ ▝▝    ▘▘ ▝▝{RESET}",  # Two aliens feet - Width: 14 chars
    ]


def _get_active_model_display_name() -> str:
    """Detect the active Claude Code model and return a human-friendly name.

    Detection priority:
    1. ANTHROPIC_MODEL env var (set by claude-mpm config or user environment)
    2. Project .claude/settings.local.json "model" key
    3. Project .claude/settings.json "model" key
    4. Global ~/.claude/settings.json "model" key
    5. Fall back to "Default" (Claude Code's own default)

    Returns:
        Human-friendly model name for display (e.g. "Opus 4.6", "Sonnet", "Default")
    """
    # Alias map: raw model identifiers -> display names
    MODEL_DISPLAY: dict[str, str] = {
        # Short aliases
        "opus": "Opus",
        "sonnet": "Sonnet",
        "haiku": "Haiku",
        # Specific versioned models (most recent first — ordering matters for prefix matching)
        "claude-opus-4-6": "Opus 4.6",
        "claude-sonnet-4-6": "Sonnet 4.6",
        "claude-haiku-4-6": "Haiku 4.6",
        "claude-opus-4-5": "Opus 4.5",
        "claude-sonnet-4-5": "Sonnet 4.5",
        "claude-haiku-4-5": "Haiku 4.5",
        "claude-3-7-sonnet-20250219": "Sonnet 3.7",
        "claude-3-5-sonnet-20241022": "Sonnet 3.5",
        "claude-3-5-haiku-20241022": "Haiku 3.5",
        "claude-3-opus-20240229": "Opus 3",
    }

    def _friendly(raw: str) -> str:
        """Convert a raw model identifier to a display name."""
        if not raw:
            return "Default"
        lower = raw.lower().strip()
        # Exact match first
        if lower in MODEL_DISPLAY:
            return MODEL_DISPLAY[lower]
        # Prefix match for dated variants (e.g. "claude-opus-4-6-20260101")
        for key, display in MODEL_DISPLAY.items():
            if lower.startswith(key):
                return display
        # Fallback: show the raw model identifier, truncated for display
        return raw[:30] if len(raw) > 30 else raw

    # 1. ANTHROPIC_MODEL env var
    env_model = os.environ.get("ANTHROPIC_MODEL", "").strip()
    if env_model:
        return _friendly(env_model)

    # 2-4. Check Claude settings files (project-local first, then global)
    settings_paths = [
        Path.cwd() / ".claude" / "settings.local.json",
        Path.cwd() / ".claude" / "settings.json",
        Path.home() / ".claude" / "settings.json",
    ]
    for settings_path in settings_paths:
        try:
            if settings_path.exists():
                with open(settings_path) as f:
                    data = json.load(f)
                model_val = data.get("model", "").strip()
                if model_val:
                    return _friendly(model_val)
        except (json.JSONDecodeError, OSError, KeyError, ValueError) as exc:
            logger.debug("Could not read model from %s: %s", settings_path, exc)
            continue

    # 5. Claude Code's own default (no override configured anywhere)
    return "Default"


def _format_logging_status(logging_level: str) -> str:
    """Format logging status with helpful indicator."""
    if logging_level == "OFF":
        return "Logging: OFF (default)"
    if logging_level == "DEBUG":
        return f"Logging: {logging_level} (verbose)"
    return f"Logging: {logging_level}"


def _get_cwd_display(max_width: int = 40) -> str:
    """Get current working directory, truncated if needed."""
    cwd = str(Path.cwd())

    if len(cwd) <= max_width:
        return cwd

    # Truncate from the left with ellipsis
    return "..." + cwd[-(max_width - 3) :]


def _collect_scope_names(scope: str) -> tuple[set[str], set[str]]:
    """Collect agent and skill identifier sets for a given scope.

    Args:
        scope: "project" (Path.cwd()/.claude/) or "user" (Path.home()/.claude/)

    Returns:
        Tuple of (agent_name_set, skill_name_set) with lowercased identifiers
    """
    try:
        base = Path.cwd() / ".claude" if scope == "project" else Path.home() / ".claude"

        # Collect agent stems: *.md files excluding README, INSTRUCTIONS, dotfiles
        agent_names: set[str] = set()
        agents_dir = base / "agents"
        if agents_dir.is_dir():
            for f in agents_dir.glob("*.md"):
                if not f.name.startswith(("README", "INSTRUCTIONS", ".")):
                    agent_names.add(f.stem.lower())

        # Collect skill dir names: top-level dirs containing SKILL.md or skill.md
        # Exclude git source repos (dirs containing .git)
        skill_names: set[str] = set()
        skills_dir = base / "skills"
        if skills_dir.is_dir():
            for item in skills_dir.iterdir():
                if not item.is_dir():
                    continue
                if (item / ".git").exists():
                    continue
                if (item / "SKILL.md").exists() or (item / "skill.md").exists():
                    skill_names.add(item.name.lower())

        return (agent_names, skill_names)
    except Exception:
        return (set(), set())


def _count_scope_assets(scope: str) -> tuple[int, int]:
    """Count deployed agents and skills for a given scope.

    Args:
        scope: "project" (Path.cwd()/.claude/) or "user" (Path.home()/.claude/)

    Returns:
        Tuple of (agent_count, skill_count)
    """
    agent_names, skill_names = _collect_scope_names(scope)
    return (len(agent_names), len(skill_names))


def _format_scope_counts(
    prefix: str, agent_count: int, skill_count: int, max_width: int
) -> str:
    """Format scope counts with progressive truncation.

    Levels: "proj: 48 agents, 189 skills" -> "proj: 48a, 189s" -> "proj: 48a 189s"
    """
    available = max_width - 2  # padding margin

    if agent_count == 0 and skill_count == 0:
        return f"{prefix}: none"

    # Level 1: Full format
    parts = []
    if agent_count > 0:
        parts.append(f"{agent_count} agent{'s' if agent_count != 1 else ''}")
    if skill_count > 0:
        parts.append(f"{skill_count} skill{'s' if skill_count != 1 else ''}")
    full = f"{prefix}: {', '.join(parts)}"
    if len(full) <= available:
        return full

    # Level 2: Abbreviated
    parts = []
    if agent_count > 0:
        parts.append(f"{agent_count}a")
    if skill_count > 0:
        parts.append(f"{skill_count}s")
    abbrev = f"{prefix}: {', '.join(parts)}"
    if len(abbrev) <= available:
        return abbrev

    # Level 3: Minimal (no comma)
    parts = []
    if agent_count > 0:
        parts.append(f"{agent_count}a")
    if skill_count > 0:
        parts.append(f"{skill_count}s")
    return f"{prefix}: {' '.join(parts)}"


def _format_two_column_line(
    left: str, right: str, left_panel_width: int, right_panel_width: int
) -> str:
    """
    Format a two-column line with precise width control.

    Args:
        left: Left panel content (will be centered in left_panel_width)
        right: Right panel content (will be left-aligned in right_panel_width)
        left_panel_width: Width of left panel in characters
        right_panel_width: Width of right panel in characters

    Returns:
        Formatted line with exact character widths
    """
    # Strip ANSI codes for width calculation
    left_display = _strip_ansi_codes(left)
    right_display = _strip_ansi_codes(right)

    # Calculate actual display width
    left_width = _get_display_width(left_display)
    right_width = _get_display_width(right_display)

    # Calculate padding for left panel (centered)
    left_padding = (left_panel_width - left_width) // 2
    right_padding = left_panel_width - left_width - left_padding

    # Format left panel with centering
    left_formatted = " " * left_padding + left + " " * right_padding

    # Format right panel (left-aligned)
    # Right panel content should exactly fill right_panel_width
    right_formatted = right + " " * (right_panel_width - right_width)

    return f"│{left_formatted}│ {right_formatted}│"


def display_startup_banner(
    version: str,
    logging_level: str,
    applied_migrations: List[str] | None = None,
) -> None:
    """
    Display startup banner with welcome message and info.

    Banner dynamically adjusts to terminal width with cyan header highlight
    (Claude Code style).

    Args:
        version: Claude MPM version string
        logging_level: Current logging level (OFF/INFO/DEBUG)
        applied_migrations: List of migration descriptions applied this session.
            If None or empty, migration section is not shown.
    """
    # Note: Banner is shown BEFORE "Launching Claude..." progress bar (in cli/__init__.py)
    # This ensures users see welcome message before background services start
    print()  # Empty line before banner

    # Get terminal width and calculate panel sizes
    terminal_width = _get_terminal_width()
    left_panel_width = int(terminal_width * 0.25)  # ~25% for left panel
    # -4 accounts for: 2 borders (│) + 2 column separators (│ and space)
    right_panel_width = terminal_width - left_panel_width - 4

    username = _get_username()

    # Get recent git commits for "Recent activity" section
    recent_commits = _get_recent_commits(max_commits=3)

    # Build header line with cyan highlight (Claude Code style)
    header = f"─── Claude MPM v{version} "
    header_padding = "─" * (terminal_width - len(header) - 2)  # -2 for ╭╮
    top_line = f"╭{CYAN}{header}{header_padding}{RESET}╮"

    # Build content lines (plain text, no color)
    lines = []

    # Migration section (only if migrations were applied this session)
    if applied_migrations:
        migration_count = len(applied_migrations)
        migration_header = f"Migrations applied: {migration_count}"
        lines.append(
            _format_two_column_line(
                "", migration_header, left_panel_width, right_panel_width
            )
        )
        # Show each migration as a bullet point
        for migration_desc in applied_migrations:
            # Truncate if needed
            max_width = right_panel_width - 4  # Leave room for bullet
            if len(migration_desc) > max_width:
                migration_desc = migration_desc[: max_width - 3] + "..."
            lines.append(
                _format_two_column_line(
                    "", f"  - {migration_desc}", left_panel_width, right_panel_width
                )
            )
        # Add separator after migrations section
        separator = "-" * right_panel_width
        lines.append(
            _format_two_column_line("", separator, left_panel_width, right_panel_width)
        )

    # Line 1: Empty left | "Recent activity" right
    lines.append(
        _format_two_column_line(
            "", "Recent activity", left_panel_width, right_panel_width
        )
    )

    # Lines 2-4: Welcome message + commit activity (3 lines total)
    welcome_msg = f"Welcome back {username}!"

    # Truncate commits to fit right panel width (leave 2 chars margin)
    max_commit_width = right_panel_width - 2
    truncated_commits = []
    for commit in recent_commits:
        if len(commit) > max_commit_width:
            truncated_commits.append(commit[: max_commit_width - 3] + "...")
        else:
            truncated_commits.append(commit)

    # Line 2: Welcome message | First commit or "No recent activity"
    if truncated_commits:
        lines.append(
            _format_two_column_line(
                welcome_msg, truncated_commits[0], left_panel_width, right_panel_width
            )
        )
    else:
        lines.append(
            _format_two_column_line(
                welcome_msg, "No recent activity", left_panel_width, right_panel_width
            )
        )

    # Line 3: Empty left | Second commit or empty
    if len(truncated_commits) >= 2:
        lines.append(
            _format_two_column_line(
                "", truncated_commits[1], left_panel_width, right_panel_width
            )
        )
    else:
        lines.append(
            _format_two_column_line("", "", left_panel_width, right_panel_width)
        )

    # Line 4: Empty left | Third commit or empty
    if len(truncated_commits) >= 3:
        lines.append(
            _format_two_column_line(
                "", truncated_commits[2], left_panel_width, right_panel_width
            )
        )
    else:
        lines.append(
            _format_two_column_line("", "", left_panel_width, right_panel_width)
        )

    # Line 5: Empty left | separator right
    separator = "─" * right_panel_width
    lines.append(
        _format_two_column_line("", separator, left_panel_width, right_panel_width)
    )

    # Line 6: Alien art line 1 | "What's new"
    alien_art = _get_alien_art()
    lines.append(
        _format_two_column_line(
            alien_art[0], "What's new", left_panel_width, right_panel_width
        )
    )

    # Lines 7-8: More alien art | changelog highlights
    max_highlight_width = right_panel_width - 5  # Leave some margin
    highlights = _parse_changelog_highlights(max_items=3, max_width=max_highlight_width)

    if len(highlights) >= 1:
        lines.append(
            _format_two_column_line(
                alien_art[1], highlights[0], left_panel_width, right_panel_width
            )
        )
    else:
        lines.append(
            _format_two_column_line(
                alien_art[1], "", left_panel_width, right_panel_width
            )
        )

    if len(highlights) >= 2:
        lines.append(
            _format_two_column_line(
                alien_art[2], highlights[1], left_panel_width, right_panel_width
            )
        )
    else:
        lines.append(
            _format_two_column_line(
                alien_art[2], "", left_panel_width, right_panel_width
            )
        )

    # Line 9: Empty | third highlight or link
    if len(highlights) >= 3:
        lines.append(
            _format_two_column_line(
                "", highlights[2], left_panel_width, right_panel_width
            )
        )
    else:
        lines.append(
            _format_two_column_line(
                "", "/mpm-help for more", left_panel_width, right_panel_width
            )
        )

    # === Bottom section: Model, scope counts, CWD, commands ===
    separator = "─" * right_panel_width
    active_model = _get_active_model_display_name()

    # Collect name sets once, derive all counts
    proj_agent_names, proj_skill_names = _collect_scope_names("project")
    user_agent_names, user_skill_names = _collect_scope_names("user")
    total_agent_names = proj_agent_names | user_agent_names
    total_skill_names = proj_skill_names | user_skill_names

    proj_agents, proj_skills = len(proj_agent_names), len(proj_skill_names)
    user_agents, user_skills = len(user_agent_names), len(user_skill_names)
    total_agents, total_skills = len(total_agent_names), len(total_skill_names)

    # Line 10: Model | separator
    lines.append(
        _format_two_column_line(
            active_model, separator, left_panel_width, right_panel_width
        )
    )

    # Line 11: Project scope counts | MPM Commands header
    proj_line = _format_scope_counts("proj", proj_agents, proj_skills, left_panel_width)
    lines.append(
        _format_two_column_line(
            proj_line, "MPM Commands", left_panel_width, right_panel_width
        )
    )

    # Line 12: User scope counts | /mpm command
    user_line = _format_scope_counts("user", user_agents, user_skills, left_panel_width)
    lines.append(
        _format_two_column_line(
            user_line,
            "  /mpm        - MPM overview",
            left_panel_width,
            right_panel_width,
        )
    )

    # Line 13: Total (deduplicated) counts | /mpm-agents command
    total_line = _format_scope_counts(
        "total", total_agents, total_skills, left_panel_width
    )
    lines.append(
        _format_two_column_line(
            total_line,
            "  /mpm-agents - Show agents",
            left_panel_width,
            right_panel_width,
        )
    )

    # Line 14: CWD | /mpm-doctor command
    cwd = _get_cwd_display(left_panel_width - 2)
    lines.append(
        _format_two_column_line(
            cwd,
            "  /mpm-doctor - Run diagnostics",
            left_panel_width,
            right_panel_width,
        )
    )

    # Line 15: Empty | empty
    lines.append(_format_two_column_line("", "", left_panel_width, right_panel_width))

    # Line 16: Empty | empty
    lines.append(_format_two_column_line("", "", left_panel_width, right_panel_width))

    # Line 17: Empty | autocomplete tip
    lines.append(
        _format_two_column_line(
            "", "Type / for autocomplete", left_panel_width, right_panel_width
        )
    )

    # Build bottom line (plain text, no color)
    bottom_line = f"╰{'─' * (terminal_width - 2)}╯"

    # Print banner (only header has cyan color)
    print(top_line)
    for line in lines:
        print(line)
    print(bottom_line)
    print()  # Empty line after banner


def should_show_banner(args) -> bool:
    """
    Determine if startup banner should be displayed.

    Skip banner for: --help, --version, info, doctor, config, configure, oauth, setup, slack commands
    Also skip for fast read-only commands like `agents list` and `skills list`
    """
    # Check for help/version flags
    if hasattr(args, "help") and args.help:
        return False
    if hasattr(args, "version") and args.version:
        return False

    # Check for commands that should skip banner
    # Lightweight commands are fast utilities that should run immediately
    from claude_mpm.cli.command_config import is_lightweight_command

    if hasattr(args, "command") and is_lightweight_command(args.command):
        return False

    # Skip banner for fast read-only subcommands
    # These only read cached data and should display output immediately
    if hasattr(args, "command"):
        command = args.command

        # Skip for agents list
        if command == "agents":
            agents_cmd = getattr(args, "agents_command", None)
            if agents_cmd == "list":
                return False

        # Skip for skills list
        if command == "skills":
            skills_cmd = getattr(args, "skills_command", None)
            if skills_cmd == "list":
                return False

    return True
