"""Main Installer Orchestrator for AiCippy.

Coordinates the complete installation workflow including:
- Prerequisite checks and dependency installation
- Permission configuration
- Environment variable setup
- Version management and auto-updates
- Progress visualization
"""

from __future__ import annotations

import asyncio
import re
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Final

from rich.console import Console
from rich.progress import (
    BarColumn,
    Progress,
    SpinnerColumn,
    TextColumn,
    TimeElapsedColumn,
)

from aicippy import __version__ as CURRENT_VERSION
from aicippy.config import get_settings
from aicippy.installer.dependency_manager import (
    check_dependencies,
    compare_versions,
    install_all_dependencies,
)
from aicippy.installer.dependency_manager import (
    verify_installation as verify_deps,
)
from aicippy.installer.env_manager import (
    get_env_export_commands,
    setup_all_env_variables,
    verify_env_variables,
)
from aicippy.installer.permission_manager import (
    setup_all_directories,
    verify_all_permissions,
)
from aicippy.installer.platform_detect import (
    MIN_DISK_SPACE_GB,
    MIN_PYTHON_VERSION,
    PlatformInfo,
    detect_platform,
)
from aicippy.utils.logging import get_logger
from aicippy.utils.network import check_pypi_reachable

logger = get_logger(__name__)

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

# Fallback URL kept for future use; PyPI via pip is the primary version check method.
VERSION_CHECK_URL: Final[str] = get_settings().version_check_url
PYPI_PACKAGE_NAME: Final[str] = "aicippy"

# Regex for validating version strings (digits and dots, e.g. "1.6.8")
_VERSION_RE: Final[re.Pattern[str]] = re.compile(
    r"^\d+(?:\.\d+)*$",
)

# Module-level version check cache (timestamp, VersionInfo)
_version_cache: dict[str, tuple[float, VersionInfo]] = {}
_VERSION_CACHE_TTL_SECONDS: Final[float] = 3600.0  # 1 hour
_LOG_MAX_STDERR_CHARS: Final[int] = 200


@dataclass
class InstallationResult:
    """Result of installation process."""

    success: bool
    version: str
    phases_completed: list[str]
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    requires_restart: bool = False
    env_commands: list[str] = field(default_factory=list)


@dataclass
class VersionInfo:
    """Version information.

    Attributes:
        current: The currently installed version string.
        latest: The latest available version, or None if unknown.
        update_available: True when a newer version exists on PyPI.
        check_failed: True when the version check could not complete
            (network unreachable, pip error, timeout). Callers should
            treat this as "unknown" rather than "up to date".
        release_notes: Optional release notes text.

    """

    current: str
    latest: str | None
    update_available: bool
    check_failed: bool = False
    release_notes: str | None = None


def _is_valid_version(version: str) -> bool:
    """Return True if *version* looks like a valid version (digits and dots).

    Examples of valid versions: ``1.0.0``, ``2.3``, ``1.6.8.post1`` is
    rejected (only digits+dots pass).
    """
    return bool(_VERSION_RE.match(version))


def _get_cached_version() -> VersionInfo | None:
    """Return cached VersionInfo if it exists and is fresh, else None."""
    entry = _version_cache.get("latest")
    if entry is None:
        return None
    cached_time, cached_info = entry
    if (time.monotonic() - cached_time) < _VERSION_CACHE_TTL_SECONDS:
        return cached_info
    # Stale -- discard
    _version_cache.pop("latest", None)
    return None


def _set_cached_version(info: VersionInfo) -> None:
    """Store *info* in the module-level cache with a monotonic timestamp."""
    _version_cache["latest"] = (time.monotonic(), info)


async def check_latest_version() -> VersionInfo:
    """Check for the latest available version.

    Performs a pre-flight network check, uses a 1-hour cache, validates
    parsed version strings, and clearly indicates when the check failed
    (via ``VersionInfo.check_failed``).

    Returns:
        VersionInfo with current and latest versions.

    """
    # 1. Return from cache if fresh
    cached = _get_cached_version()
    if cached is not None:
        logger.debug("version_check_cache_hit")
        return cached

    failed_info = VersionInfo(
        current=CURRENT_VERSION,
        latest=None,
        update_available=False,
        check_failed=True,
    )

    # 2. Pre-flight: verify PyPI is reachable before spawning pip
    if not check_pypi_reachable():
        logger.warning(
            "version_check_skipped_network_unreachable",
            reason="pypi.org is not reachable",
        )
        return failed_info

    # 3. Query pip for available versions
    try:
        result = subprocess.run(
            [
                sys.executable,
                "-m",
                "pip",
                "index",
                "versions",
                PYPI_PACKAGE_NAME,
            ],
            capture_output=True,
            text=True,
            timeout=30,
            check=False,
        )

        if result.returncode != 0:
            logger.warning(
                "version_check_pip_failed",
                returncode=result.returncode,
                stderr=result.stderr.strip()[:_LOG_MAX_STDERR_CHARS],
            )
            return failed_info
    except subprocess.TimeoutExpired:
        logger.warning("version_check_timed_out", timeout_seconds=30)
        return failed_info
    except Exception as exc:
        logger.warning("version_check_failed", error=str(exc))
        return failed_info
    else:
        # Parse pip output:
        #   aicippy (1.0.0)
        #   Available versions: 1.0.0, 0.9.0, ...
        output = result.stdout
        if "Available versions:" in output:
            versions_section = output.split("Available versions:")[1]
            first_line = versions_section.split("\n")[0].strip()
            versions = [v.strip() for v in first_line.split(",") if v.strip()]
            if versions:
                latest = versions[0]

                # 4. Validate parsed version format
                if not _is_valid_version(latest):
                    logger.warning("version_check_invalid_format", raw_version=latest)
                    return failed_info

                is_update = compare_versions(CURRENT_VERSION, latest) < 0
                info = VersionInfo(
                    current=CURRENT_VERSION,
                    latest=latest,
                    update_available=is_update,
                    check_failed=False,
                )
                _set_cached_version(info)
                return info

        # pip succeeded but output was unexpected
        logger.warning("version_check_unexpected_output", stdout=output.strip()[:300])
        return failed_info


def _verify_upgrade(target: str) -> tuple[bool, str]:
    """Verify that the package was upgraded successfully."""
    try:
        verify_result = subprocess.run(
            [sys.executable, "-m", "pip", "show", PYPI_PACKAGE_NAME],
            capture_output=True,
            text=True,
            timeout=30,
            check=False,
        )

        if verify_result.returncode != 0:
            logger.warning(
                "upgrade_verification_pip_show_failed",
                returncode=verify_result.returncode,
            )
            return (
                False,
                f"Upgrade ran but post-install verification failed (pip show exit {verify_result.returncode})",
            )

        # Parse "Version: X.Y.Z" from pip show output
        installed_version: str | None = None
        for line in verify_result.stdout.splitlines():
            if line.startswith("Version:"):
                installed_version = line.split(":", 1)[1].strip()
                break

        if installed_version is None:
            logger.warning(
                "upgrade_verification_no_version",
                pip_show_stdout=verify_result.stdout.strip()[:_LOG_MAX_STDERR_CHARS],
            )
            return (
                False,
                "Upgrade ran but could not determine installed version",
            )

        if installed_version != target:
            logger.warning(
                "upgrade_verification_mismatch",
                expected=target,
                actual=installed_version,
            )
            return (
                False,
                f"Version mismatch after upgrade: expected {target}, got {installed_version}",
            )

    except subprocess.TimeoutExpired:
        logger.warning("upgrade_verification_timed_out")
        return (
            False,
            "Post-upgrade verification timed out",
        )
    except Exception as exc:
        logger.warning("upgrade_verification_error", error=str(exc))
        return (
            False,
            f"Post-upgrade verification error: {exc}",
        )

    return True, f"Successfully upgraded AiCippy to {target}"


async def perform_upgrade(silent: bool = True) -> tuple[bool, str]:
    """Perform package upgrade.

    Validates the target version on PyPI before upgrading, checks pip
    return codes, and verifies the installed version post-upgrade.

    Args:
        silent: Run in silent mode without output.

    Returns:
        Tuple of (success, message).

    """
    # 1. Pre-upgrade validation: confirm target version exists on PyPI
    version_info = await check_latest_version()
    target = version_info.latest

    if version_info.check_failed or target is None:
        logger.warning(
            "upgrade_skipped_no_target_version",
            check_failed=version_info.check_failed,
        )
        return False, "Cannot determine target version (PyPI unreachable or check failed)"

    if not version_info.update_available:
        return True, f"Already at latest version {CURRENT_VERSION}"

    # 2. Perform the pip upgrade
    try:
        cmd = [
            sys.executable,
            "-m",
            "pip",
            "install",
            "--upgrade",
            f"{PYPI_PACKAGE_NAME}=={target}",
        ]
        if silent:
            cmd.extend(
                [
                    "--quiet",
                    "--disable-pip-version-check",
                    "--no-warn-script-location",
                ],
            )

        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=300,
            stdin=subprocess.DEVNULL,
            check=False,
        )

        if result.returncode != 0:
            stderr_snippet = result.stderr.strip()[:300]
            logger.warning(
                "upgrade_pip_failed",
                returncode=result.returncode,
                stderr=stderr_snippet,
            )
            return False, f"Upgrade failed (exit {result.returncode}): {stderr_snippet}"

    except subprocess.TimeoutExpired:
        logger.warning("upgrade_timed_out", timeout_seconds=300)
        return False, "Upgrade timed out after 300 seconds"
    except Exception as exc:
        logger.warning("upgrade_error", error=str(exc))
        return False, f"Upgrade error: {exc}"
    else:
        # 3. Post-upgrade verification: confirm new version is installed
        verify_success, verify_msg = _verify_upgrade(target)
        if not verify_success:
            return False, verify_msg

        # Invalidate the version cache after a successful upgrade
        _version_cache.pop("latest", None)

        return True, verify_msg


def check_prerequisites(
    platform_info: PlatformInfo,
) -> tuple[bool, list[str]]:
    """Check all prerequisites for installation.

    Args:
        platform_info: Platform information.

    Returns:
        Tuple of (all_met, list of issues).

    """
    issues: list[str] = []

    # Check Python version
    if not platform_info.python_meets_minimum:
        py_ver = platform_info.python_version
        min_ver = MIN_PYTHON_VERSION
        issues.append(
            f"Python {min_ver[0]}.{min_ver[1]}+ required, found {py_ver[0]}.{py_ver[1]}.{py_ver[2]}",
        )

    # Check pip availability
    if platform_info.pip_path is None:
        issues.append("pip not found. Please install pip first.")

    # Check disk space
    if not platform_info.disk_space_sufficient:
        issues.append(
            f"Insufficient disk space: {platform_info.disk_space_gb:.1f}GB available, {MIN_DISK_SPACE_GB}GB required",
        )

    # Add any platform-specific errors
    issues.extend(platform_info.errors)

    return len(issues) == 0, issues


async def installation_phase_generator(
    platform_info: PlatformInfo,
    include_deps: bool = True,
) -> AsyncIterator[tuple[str, int, str]]:
    """Generate installation phases with progress updates.

    Args:
        platform_info: Platform information.
        include_deps: Whether to install dependencies.

    Yields:
        Tuples of (phase_id, progress_percent, status_message).

    """
    errors: list[str] = []
    warnings: list[str] = []

    # Phase 1: Initialization
    yield "init", 0, "Starting installation..."
    yield "init", 50, "Detecting platform..."
    yield "init", 100, "Platform detected"

    # Phase 2: Prerequisites
    yield "prereq", 0, "Checking prerequisites..."
    prereq_ok, prereq_issues = check_prerequisites(platform_info)

    if not prereq_ok:
        for issue in prereq_issues:
            errors.append(issue)
        yield "prereq", 100, "Prerequisites check failed"
        return

    yield "prereq", 100, "All prerequisites met"

    # Phase 3: Dependencies
    if include_deps:
        yield "deps", 0, "Checking dependencies..."
        dep_check = check_dependencies()

        if not dep_check.all_satisfied:
            yield "deps", 10, "Installing dependencies..."

            success, dep_errors = install_all_dependencies(
                platform_info,
                progress_callback=None,
            )

            if not success:
                errors.extend(dep_errors)

            yield "deps", 100, "Dependencies installed"
        else:
            yield "deps", 100, "All dependencies satisfied"
    else:
        yield "deps", 100, "Skipping dependency check"

    # Phase 4: Permissions
    yield "perms", 0, "Configuring permissions..."
    success, perm_results = setup_all_directories(platform_info)

    for result in perm_results:
        if not result.success:
            errors.append(result.message)

    yield "perms", 100, "Permissions configured"

    # Phase 5: Environment
    yield "env", 0, "Setting up environment..."
    success, env_results, requires_restart = setup_all_env_variables(
        platform_info,
    )

    for env_result in env_results:
        if not env_result.success and env_result.error:
            warnings.append(f"{env_result.name}: {env_result.error}")

    if requires_restart:
        warnings.append("Shell restart required for PATH changes")

    yield "env", 100, "Environment configured"

    # Phase 6: Installation finalization
    yield "install", 0, "Finalizing..."
    yield "install", 100, "Installation complete"

    # Phase 7: Verification
    yield "verify", 0, "Verifying installation..."

    deps_ok, deps_msg = verify_deps()
    if not deps_ok:
        warnings.append(deps_msg)

    perms_ok, perm_issues = verify_all_permissions(platform_info)
    if not perms_ok:
        for _path, issue in perm_issues[:2]:
            warnings.append(issue)

    env_ok, missing_vars = verify_env_variables(platform_info)
    if not env_ok:
        for var in missing_vars[:2]:
            warnings.append(f"{var} not set (restart may be needed)")

    yield "verify", 100, "Verification complete"


async def install_aicippy(
    console: Console | None = None,
    show_progress: bool = True,
    include_deps: bool = True,
    silent: bool = False,
    _auto_accept: bool = True,
) -> InstallationResult:
    """Perform complete AiCippy installation.

    Args:
        console: Optional Rich console.
        show_progress: Whether to show progress UI.
        include_deps: Whether to install dependencies.
        silent: Run in background silent mode (no output, auto-accept).
        _auto_accept: Automatically accept all prompts/terms.

    Returns:
        InstallationResult with status.

    """
    # Silent mode overrides show_progress
    if silent:
        show_progress = False

    console = console or Console(quiet=silent)
    platform_info = detect_platform()

    errors: list[str] = []
    warnings: list[str] = []
    phases_completed: list[str] = []
    requires_restart = False
    completed_pct = 100

    # Phase weights for overall progress calculation
    phase_weights = {
        "init": 5,
        "prereq": 10,
        "deps": 40,
        "perms": 10,
        "env": 15,
        "install": 15,
        "verify": 5,
    }
    total_weight = sum(phase_weights.values())

    def _calculate_overall(phase_id: str, phase_pct: int) -> int:
        """Calculate overall progress from phase and its progress."""
        phase_order = list(phase_weights.keys())
        if phase_id not in phase_order:
            return 0
        idx = phase_order.index(phase_id)
        completed_weight = sum(phase_weights[phase_order[i]] for i in range(idx))
        current_contribution = phase_weights[phase_id] * (phase_pct / completed_pct)
        return int((completed_weight + current_contribution) / total_weight * completed_pct)

    if show_progress:
        with Progress(
            SpinnerColumn(),
            TextColumn(
                "[progress.description]{task.description}",
            ),
            BarColumn(
                bar_width=40,
                complete_style="#667eea",
                finished_style="#10b981",
            ),
            TextColumn(
                "[progress.percentage]{task.percentage:>3.0f}%",
            ),
            TimeElapsedColumn(),
            console=console,
            transient=True,
        ) as progress_bar:
            task = progress_bar.add_task(
                "Installing AiCippy...",
                total=completed_pct,
            )

            try:
                async for phase, pct, status in installation_phase_generator(
                    platform_info,
                    include_deps,
                ):
                    overall = _calculate_overall(phase, pct)

                    phase_labels = {
                        "init": "Detecting platform...",
                        "prereq": "Checking prerequisites...",
                        "deps": "Installing dependencies...",
                        "perms": "Configuring permissions...",
                        "env": "Setting up environment...",
                        "install": "Finalizing...",
                        "verify": "Verifying...",
                    }
                    label = phase_labels.get(
                        phase,
                        "Installing AiCippy...",
                    )
                    progress_bar.update(
                        task,
                        description=label,
                        completed=overall,
                    )

                    if phase not in phases_completed and pct == completed_pct:
                        phases_completed.append(phase)

                    # Collect errors from the prereq phase
                    if "failed" in status.lower() and phase == "prereq":
                        prereq_ok, prereq_issues = check_prerequisites(platform_info)
                        if not prereq_ok:
                            errors.extend(prereq_issues)

            except Exception as exc:
                errors.append(f"Installation error: {exc}")

            # Ensure bar shows 100% on completion
            if not errors:
                progress_bar.update(
                    task,
                    description="Complete",
                    completed=completed_pct,
                )

        # Silent completion — progress bar already shows "Complete" at 100%.
        # No additional text output; the progress bar is the only visual feedback.

    else:
        # Silent installation
        async for phase, pct, _status in installation_phase_generator(platform_info, include_deps):
            if phase not in phases_completed and pct == completed_pct:
                phases_completed.append(phase)

    # Get environment export commands for user reference
    env_commands = get_env_export_commands(platform_info)

    return InstallationResult(
        success=len(errors) == 0,
        version=CURRENT_VERSION,
        phases_completed=phases_completed,
        errors=errors,
        warnings=warnings,
        requires_restart=requires_restart,
        env_commands=env_commands,
    )


async def uninstall_aicippy(
    console: Console | None = None,
    remove_config: bool = False,
) -> tuple[bool, str]:
    """Uninstall AiCippy.

    Args:
        console: Optional Rich console.
        remove_config: Whether to remove configuration files.

    Returns:
        Tuple of (success, message).

    """
    console = console or Console()
    platform_info = detect_platform()

    try:
        result = subprocess.run(
            [
                sys.executable,
                "-m",
                "pip",
                "uninstall",
                "-y",
                PYPI_PACKAGE_NAME,
            ],
            capture_output=True,
            text=True,
            timeout=120,
            check=False,
        )

        if result.returncode != 0:
            logger.warning(
                "uninstall_failed",
                returncode=result.returncode,
                stderr=result.stderr.strip()[:_LOG_MAX_STDERR_CHARS],
            )
            return False, f"Uninstall failed: {result.stderr}"

        # Optionally remove config
        if remove_config:
            config_path = platform_info.config_path
            cache_path = platform_info.cache_path

            if config_path.exists():
                try:
                    shutil.rmtree(config_path)
                except OSError as rmtree_err:
                    logger.warning(
                        "config_removal_failed",
                        path=str(config_path),
                        error=str(rmtree_err),
                    )

            if cache_path.exists():
                try:
                    shutil.rmtree(cache_path)
                except OSError as rmtree_err:
                    logger.warning(
                        "cache_removal_failed",
                        path=str(cache_path),
                        error=str(rmtree_err),
                    )

    except subprocess.TimeoutExpired:
        logger.warning("uninstall_timed_out", timeout_seconds=120)
        return False, "Uninstall timed out"
    except Exception as exc:
        logger.warning("uninstall_error", error=str(exc))
        return False, f"Uninstall error: {exc}"
    else:
        return True, "AiCippy uninstalled successfully"


async def upgrade_aicippy(
    console: Console | None = None,
    check_only: bool = False,
) -> tuple[bool, str, VersionInfo]:
    """Check for and optionally perform upgrade.

    Args:
        console: Optional Rich console.
        check_only: If True, only check for updates without installing.

    Returns:
        Tuple of (success, message, version_info).

    """
    console = console or Console()

    # Check version
    version_info = await check_latest_version()

    if version_info.check_failed:
        return (
            True,
            "Could not check for updates (network or PyPI unavailable)",
            version_info,
        )

    if not version_info.update_available:
        return (
            True,
            f"AiCippy {version_info.current} is up to date",
            version_info,
        )

    if check_only:
        msg = f"Update available: {version_info.current} -> {version_info.latest}"
        return True, msg, version_info

    # Perform upgrade
    success, message = await perform_upgrade()

    if success:
        # After pip upgrade, the new code is on disk but this process
        # still has the old modules loaded. Report the target version.
        return (
            True,
            f"Upgraded to {version_info.latest}",
            VersionInfo(
                current=version_info.latest or CURRENT_VERSION,
                latest=version_info.latest,
                update_available=False,
                check_failed=False,
            ),
        )

    return False, message, version_info


def verify_installation() -> tuple[bool, list[str]]:
    """Verify that AiCippy is properly installed.

    Returns:
        Tuple of (success, list of issues).

    """
    issues: list[str] = []
    platform_info = detect_platform()

    # Check dependencies
    deps_ok, deps_msg = verify_deps()
    if not deps_ok:
        issues.append(f"Dependencies: {deps_msg}")

    # Check permissions
    perms_ok, perm_issues = verify_all_permissions(platform_info)
    if not perms_ok:
        for _path, issue in perm_issues:
            issues.append(f"Permission: {issue}")

    # Check environment
    env_ok, missing_vars = verify_env_variables(platform_info)
    if not env_ok:
        for var in missing_vars:
            issues.append(f"Environment: {var}")

    # Check if aicippy command is accessible
    if shutil.which("aicippy") is None:
        issues.append("Command 'aicippy' not found in PATH (may need shell restart)")

    return len(issues) == 0, issues


def get_installation_info() -> dict[str, Any]:
    """Get information about the current installation.

    Returns:
        Dictionary with installation details.

    """
    platform_info = detect_platform()

    return {
        "version": CURRENT_VERSION,
        "python_version": ".".join(str(v) for v in platform_info.python_version),
        "platform": platform_info.os_name,
        "architecture": platform_info.architecture.name,
        "shell": platform_info.shell_type.name,
        "config_path": str(platform_info.config_path),
        "cache_path": str(platform_info.cache_path),
        "install_path": str(platform_info.user_install_path),
        "is_admin": platform_info.is_admin,
    }


# Entry point for direct execution
if __name__ == "__main__":

    async def _main() -> None:
        console = Console()
        result = await install_aicippy(
            console=console,
            show_progress=True,
        )
        if result.success:
            console.print("\nInstallation completed successfully!")
        else:
            console.print("\nInstallation failed:")
            for error in result.errors:
                console.print(f"  - {error}")

    asyncio.run(_main())
