"""Deploy node - Execute deployment via delegation scripts."""

import contextlib
import importlib
import io
import os
import stat
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any

from ..config import ConfigError, load_project_config
from ..logging_config import (
    get_logger,
    log_node_error,
    log_node_input_state,
    log_node_output_state,
    log_node_start,
    log_node_success,
)
from ..state import FirmwareState

if TYPE_CHECKING:
    from ..config import ProjectConfig

logger = get_logger(__name__)


# ============================================
# Data Classes
# ============================================


@dataclass
class DeployResult:
    """Result of deploy operation."""

    success: bool
    message: str


# ============================================
# Helper Functions
# ============================================


def _find_binary(build_dir: Path) -> Path:
    """
    Find the first executable binary in build directory.

    Args:
        build_dir: Path to build directory

    Returns:
        Path to the binary file

    Raises:
        FileNotFoundError: If no executable binary found
    """
    if not build_dir.exists():
        raise FileNotFoundError(f"Build directory does not exist: {build_dir}")

    for file in build_dir.iterdir():
        if file.is_file() and _is_executable(file):
            logger.info(f"Found binary: {file.name}")
            return file

    raise FileNotFoundError(f"No executable binary found in {build_dir}")


def _is_executable(file: Path) -> bool:
    """Check if file is executable."""
    try:
        return bool(file.stat().st_mode & stat.S_IXUSR)
    except Exception:
        return file.suffix in ["", ".exe", ".out", ".elf"] or os.access(file, os.X_OK)


def _render_remote_log_path(config: "ProjectConfig", binary_name: str) -> str:
    """
    Render remote log file path using Jinja2 template.

    Args:
        config: Project configuration
        binary_name: Executable name (e.g., "led")

    Returns:
        Rendered remote log path (e.g., "/home/root/.fwauto/logs/led_2025-11-13.log")
    """
    from datetime import datetime

    from jinja2 import Template

    template_str = config.remote_log_pattern
    template = Template(template_str)

    context = {
        "deploy_path": f"{config.deploy_path}/.fwauto/logs",
        "date": datetime.now().strftime("%Y-%m-%d"),
        "binary_name": binary_name,
        "app_name": binary_name if binary_name else "app",
    }

    return template.render(context)


def _execute_deploy(build_dir: Path, config: "ProjectConfig", binary_args: str = "") -> DeployResult:
    """
    Execute deployment using delegation script.

    Args:
        build_dir: Path to build directory containing the binary
        config: Project configuration
        binary_args: Arguments to pass to the binary

    Returns:
        DeployResult indicating success/failure
    """
    logger.info("=" * 60)
    logger.info("FWAuto Deploy: Platform-Agnostic Deployment")
    logger.info("=" * 60)

    deploy_type = config.deploy_type  # "network", "serial", or "command"
    logger.info(f"🚀 Deploy Type: {deploy_type}")

    project_root = build_dir.parent

    # Check for custom deploy script (backward compatibility)
    script_path = project_root / config.deploy_script
    if script_path.exists():
        from ..utils import execute_python_script

        logger.info(f"📦 Using custom deploy script: {script_path}")

        env_vars = {
            "BINARY_ARGS": binary_args,
            "BUILD_DIR": str(build_dir),
        }

        deployment_config = config.deployment_config
        for key, value in deployment_config.items():
            env_key = f"DEPLOY_{key.upper()}"
            env_vars[env_key] = str(value)

        returncode, output = execute_python_script(
            script_path=script_path,
            project_root=project_root,
            timeout=120,
            env_vars=env_vars,
        )

        if returncode == 0:
            return DeployResult(success=True, message="Deploy completed via script")
        else:
            return DeployResult(success=False, message=f"Deploy script failed:\n{output}")

    # Use delegation script
    script_module = f"fwauto.scripts.deploy.{deploy_type}"

    try:
        module = importlib.import_module(script_module)
    except ImportError:
        error_msg = f"Unknown deploy type: {deploy_type}"
        logger.error(f"❌ {error_msg}")
        return DeployResult(success=False, message=error_msg)

    # Switch to project directory and execute
    original_cwd = os.getcwd()
    os.chdir(project_root)

    # Set environment variables for delegation script
    os.environ["BINARY_ARGS"] = binary_args

    # Pass remote log path if available (set by deploy_node)
    remote_log_path = getattr(config, "_tmp_remote_log_path", "")
    if remote_log_path:
        os.environ["REMOTE_LOG_PATH"] = remote_log_path

    try:
        stdout_capture = io.StringIO()
        stderr_capture = io.StringIO()

        with contextlib.redirect_stdout(stdout_capture), contextlib.redirect_stderr(stderr_capture):
            returncode = module.main()

        output = stdout_capture.getvalue() + stderr_capture.getvalue()
        print(output)  # Output to console

        if returncode == 0:
            logger.info("✅ Deploy completed successfully")
            return DeployResult(success=True, message="Deploy completed")
        else:
            logger.error(f"❌ Deploy failed with exit code {returncode}")
            error_msg = output if output else f"Deploy failed with exit code {returncode}"
            return DeployResult(success=False, message=error_msg)
    finally:
        os.chdir(original_cwd)
        # Clean up environment variables
        if "BINARY_ARGS" in os.environ:
            del os.environ["BINARY_ARGS"]
        if "REMOTE_LOG_PATH" in os.environ:
            del os.environ["REMOTE_LOG_PATH"]


# ============================================
# Deploy Node
# ============================================


def deploy_node(state: FirmwareState) -> dict[str, Any]:
    """
    Deploy firmware using delegation scripts.

    Process:
    1. Validate project initialization
    2. Load project configuration
    3. Execute deploy via delegation script
    4. Update state with results
    """
    log_node_input_state("deploy", state)
    log_node_start("deploy", "deploy")

    # 1. Validate project initialization
    if not state.get("project_initialized"):
        error_msg = "Project not initialized. Run init first."
        log_node_error("deploy", error_msg)
        return {
            **state,
            "deploy_status": "error",
            "deploy_errors": [error_msg],
            "deploy_log": f"❌ {error_msg}",
        }

    project_root = state.get("project_root")
    if not project_root:
        error_msg = "Project root not set"
        log_node_error("deploy", error_msg)
        return {
            **state,
            "deploy_status": "error",
            "deploy_errors": [error_msg],
            "deploy_log": f"❌ {error_msg}",
        }

    # 2. Load project configuration
    try:
        project_root_path = Path(project_root)
        config = load_project_config(project_root_path)
        logger.info("📥 Deploying firmware...")
    except ConfigError as e:
        error_msg = f"Config error: {str(e)}"
        log_node_error("deploy", error_msg)
        return {
            **state,
            "deploy_status": "error",
            "deploy_errors": [error_msg],
            "deploy_log": f"❌ {error_msg}",
        }

    # 3. Execute deployment
    try:
        # Extract binary arguments
        command_args = state.get("command_args") or {}
        binary_args = command_args.get("binary_args", "")

        # Basic argument validation (prevent shell injection)
        if binary_args:
            dangerous_chars = [";", "|", "&", "$", "`", "\n", "\r"]
            if any(c in binary_args for c in dangerous_chars):
                error_msg = "Binary args contain dangerous shell metacharacters - rejected"
                logger.error(error_msg)
                return {
                    **state,
                    "deploy_status": "error",
                    "deploy_errors": [error_msg],
                    "deploy_log": f"❌ {error_msg}",
                }
            logger.info(f"📦 Binary arguments: {binary_args}")

        # Handle remote log configuration
        remote_log_path_full = None
        if config.remote_log_enabled:
            try:
                binary_path = _find_binary(project_root_path / "build")
                binary_name = binary_path.name
                remote_log_path_local = _render_remote_log_path(config, binary_name)
                remote_log_path_full = f"{config.board_user}@{config.board_ip}:{remote_log_path_local}"

                logger.info(f"📊 Remote log will be saved to: {remote_log_path_full}")
                config._tmp_remote_log_path = remote_log_path_local
            except Exception as e:
                logger.warning(f"Failed to setup remote log: {e}")
                remote_log_path_full = None

        # Execute deploy
        result = _execute_deploy(
            build_dir=project_root_path / "build",
            config=config,
            binary_args=binary_args,
        )

        if result.success:
            success_msg = "✅ Deploy succeeded!"
            log_node_success("deploy", success_msg)

            # Handle log path based on log_type
            final_log_path = None

            if config.log_type == "serial":
                # Serial type: start UART daemon and get log path
                try:
                    from ..uart_daemon import UartDaemon
                    from ..utils import detect_serial_port

                    daemon = UartDaemon(project_root_path)
                    # 優先使用自動偵測的 serial port，否則使用 config 設定
                    port = detect_serial_port() or config.log_serial_port
                    baudrate = config.log_baudrate

                    if port:
                        if not daemon.is_running():
                            logger.info(f"🔌 Starting UART daemon on {port} @ {baudrate}")
                            daemon.start(port, baudrate)

                            # Wait briefly for daemon to create log file
                            import time

                            time.sleep(0.5)

                        # Get the latest log path
                        log_path = daemon.get_latest_log_path()
                        if log_path:
                            final_log_path = str(log_path)
                            config.update_last_log_path(final_log_path)
                            logger.info(f"📊 UART log path: {final_log_path}")
                    else:
                        logger.warning("⚠️ Serial port not configured, skipping UART daemon")
                except Exception as e:
                    logger.warning(f"Failed to start UART daemon: {e}")

            elif remote_log_path_full:
                # Network type: use remote log path
                final_log_path = remote_log_path_full
                try:
                    config.update_last_log_path(remote_log_path_full)
                    logger.info(f"📊 Auto-populated log_file_path = {remote_log_path_full}")
                except Exception as e:
                    logger.warning(f"Failed to save log path to config: {e}")

            result_state = {
                **state,
                "deploy_status": "success",
                "deploy_errors": [],
                "deploy_log": f"{result.message}\n{success_msg}",
                "log_file_path": final_log_path,
            }

            log_node_output_state("deploy", result_state)
            return result_state
        else:
            error_msg = f"Deploy failed: {result.message}"
            log_node_error("deploy", error_msg)
            return {
                **state,
                "deploy_status": "error",
                "deploy_errors": [result.message],
                "deploy_log": f"❌ {error_msg}",
            }

    except Exception as e:
        error_msg = f"Deploy exception: {str(e)}"
        log_node_error("deploy", error_msg)
        return {
            **state,
            "deploy_status": "error",
            "deploy_errors": [str(e)],
            "deploy_log": f"❌ {error_msg}",
        }
