"""
Build manager for ESPal daemon.

Coordinates build/upload operations with serial port management and
streams output to the ring buffer.
"""

import os
import subprocess
import threading
import time
import shlex
import logging
from typing import Optional, Callable, Any
from dataclasses import dataclass
from enum import Enum

from .buffer import RingBuffer
from .serial_manager import SerialManager


logger = logging.getLogger(__name__)


class BuildState(Enum):
    """State of the build manager."""
    IDLE = "idle"
    BUILDING = "building"
    UPLOADING = "uploading"


@dataclass
class BuildResult:
    """Result of a build or upload operation."""
    success: bool
    exit_code: int
    operation: str  # "build", "upload", "run"
    duration: float


class DaemonBuildManager:
    """
    Manages build and upload operations for the daemon.

    Coordinates with:
    - Ring buffer for output streaming
    - Serial manager for port release/reclaim
    - Clients for progress notification
    """

    def __init__(
        self,
        buffer: RingBuffer,
        serial_manager: SerialManager,
        project_dir: Optional[str] = None,
        on_output: Optional[Callable[[str, str], None]] = None,
        on_complete: Optional[Callable[[BuildResult], None]] = None
    ):
        """
        Initialise the build manager.

        Args:
            buffer: Ring buffer for output streaming
            serial_manager: Serial manager for port coordination
            project_dir: Project directory (default: current directory)
            on_output: Callback for output lines (stream, line)
            on_complete: Callback when build completes
        """
        self._buffer = buffer
        self._serial_manager = serial_manager
        self._project_dir = project_dir or os.getcwd()
        self._on_output = on_output
        self._on_complete = on_complete

        # State
        self._state = BuildState.IDLE
        self._state_lock = threading.Lock()
        self._current_process: Optional[subprocess.Popen] = None
        self._build_thread: Optional[threading.Thread] = None

        # Cancel support
        self._cancel_requested = False

    @property
    def state(self) -> BuildState:
        """Current build state."""
        with self._state_lock:
            return self._state

    @property
    def is_busy(self) -> bool:
        """Whether a build is in progress."""
        return self.state != BuildState.IDLE

    @property
    def project_dir(self) -> str:
        """Project directory."""
        return self._project_dir

    @project_dir.setter
    def project_dir(self, value: str) -> None:
        """Set project directory."""
        self._project_dir = value

    def _set_state(self, state: BuildState) -> None:
        """Update state."""
        with self._state_lock:
            self._state = state
        logger.debug(f"Build state: {state.value}")

    def _write_output(self, stream: str, data: bytes) -> None:
        """Write output to buffer and notify callback."""
        self._buffer.append(stream, data)

        if self._on_output:
            try:
                text = data.decode("utf-8", errors="replace")
                for line in text.splitlines():
                    self._on_output(stream, line)
            except Exception as e:
                logger.error(f"Output callback error: {e}")

    def _run_command(
        self,
        command: list[str],
        operation: str,
        needs_port: bool = False
    ) -> BuildResult:
        """
        Run a command and capture output.

        Args:
            command: Command to run as list
            operation: Operation name for logging
            needs_port: Whether to release serial port

        Returns:
            BuildResult with outcome
        """
        start_time = time.time()

        # Insert start marker
        self._buffer.mark(f"{operation}_start")

        # Release port if needed
        if needs_port:
            logger.info("Releasing serial port for upload")
            if not self._serial_manager.release_port(timeout=5.0):
                logger.warning("Failed to release serial port")
                self._buffer.mark(f"{operation}_complete")
                return BuildResult(
                    success=False,
                    exit_code=-1,
                    operation=operation,
                    duration=time.time() - start_time
                )

        try:
            # Start process
            logger.info(f"Running: {' '.join(command)}")
            self._current_process = subprocess.Popen(
                command,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                cwd=self._project_dir
            )

            # Read output in threads
            def read_stream(stream, name):
                try:
                    for line in iter(stream.readline, b""):
                        if self._cancel_requested:
                            break
                        self._write_output(name, line)
                except Exception as e:
                    logger.error(f"Error reading {name}: {e}")
                finally:
                    stream.close()

            stdout_thread = threading.Thread(
                target=read_stream,
                args=(self._current_process.stdout, "stdout"),
                daemon=True
            )
            stderr_thread = threading.Thread(
                target=read_stream,
                args=(self._current_process.stderr, "stderr"),
                daemon=True
            )

            stdout_thread.start()
            stderr_thread.start()

            # Wait for completion
            exit_code = self._current_process.wait()

            stdout_thread.join(timeout=2.0)
            stderr_thread.join(timeout=2.0)

            success = (exit_code == 0)
            logger.info(f"{operation} completed with exit code {exit_code}")

        except Exception as e:
            logger.error(f"Error running command: {e}")
            exit_code = -1
            success = False

        finally:
            self._current_process = None

            # Reclaim port if we released it
            if needs_port:
                logger.info("Reclaiming serial port")
                self._serial_manager.reclaim_port()

                # Wait for device to reconnect
                if not self._serial_manager.wait_for_device(timeout=10.0):
                    logger.warning("Device did not reconnect after upload")

        # Insert completion marker
        self._buffer.mark(f"{operation}_complete")

        return BuildResult(
            success=success,
            exit_code=exit_code,
            operation=operation,
            duration=time.time() - start_time
        )

    def _detect_build_command(self, environment: Optional[str] = None) -> list[str]:
        """
        Detect the build command based on project type.

        Args:
            environment: Optional environment name

        Returns:
            Command list
        """
        # Check for PlatformIO
        if os.path.exists(os.path.join(self._project_dir, "platformio.ini")):
            cmd = ["pio", "run"]
            if environment:
                cmd.extend(["-e", environment])
            return cmd

        # Check for ESP-IDF
        if os.path.exists(os.path.join(self._project_dir, "CMakeLists.txt")):
            idf_path = os.environ.get("IDF_PATH")
            if idf_path:
                return ["idf.py", "build"]

        # Check for Arduino CLI
        ino_files = [f for f in os.listdir(self._project_dir) if f.endswith(".ino")]
        if ino_files:
            return ["arduino-cli", "compile"]

        # Default: just run make
        return ["make"]

    def _detect_upload_command(self, environment: Optional[str] = None) -> list[str]:
        """
        Detect the upload command based on project type.

        Args:
            environment: Optional environment name

        Returns:
            Command list
        """
        # Check for PlatformIO
        if os.path.exists(os.path.join(self._project_dir, "platformio.ini")):
            cmd = ["pio", "run", "-t", "upload"]
            if environment:
                cmd.extend(["-e", environment])
            return cmd

        # Check for ESP-IDF
        if os.path.exists(os.path.join(self._project_dir, "CMakeLists.txt")):
            idf_path = os.environ.get("IDF_PATH")
            if idf_path:
                return ["idf.py", "flash"]

        # Check for Arduino CLI
        ino_files = [f for f in os.listdir(self._project_dir) if f.endswith(".ino")]
        if ino_files:
            return ["arduino-cli", "upload"]

        return ["make", "flash"]

    def build(
        self,
        args: Optional[list[str]] = None,
        environment: Optional[str] = None,
        async_: bool = True
    ) -> Optional[BuildResult]:
        """
        Start a build operation.

        Args:
            args: Additional arguments for build command
            environment: Build environment name
            async_: Run asynchronously (returns immediately)

        Returns:
            BuildResult if synchronous, None if async
        """
        if self.is_busy:
            logger.warning("Build already in progress")
            return None

        self._set_state(BuildState.BUILDING)
        self._cancel_requested = False

        command = self._detect_build_command(environment)
        if args:
            command.extend(args)

        def do_build():
            try:
                result = self._run_command(command, "build", needs_port=False)
                if self._on_complete:
                    self._on_complete(result)
            finally:
                self._set_state(BuildState.IDLE)

        if async_:
            self._build_thread = threading.Thread(target=do_build, daemon=True)
            self._build_thread.start()
            return None
        else:
            try:
                return self._run_command(command, "build", needs_port=False)
            finally:
                self._set_state(BuildState.IDLE)

    def upload(
        self,
        args: Optional[list[str]] = None,
        environment: Optional[str] = None,
        async_: bool = True
    ) -> Optional[BuildResult]:
        """
        Start an upload operation.

        Args:
            args: Additional arguments for upload command
            environment: Build environment name
            async_: Run asynchronously (returns immediately)

        Returns:
            BuildResult if synchronous, None if async
        """
        if self.is_busy:
            logger.warning("Build already in progress")
            return None

        self._set_state(BuildState.UPLOADING)
        self._cancel_requested = False

        command = self._detect_upload_command(environment)
        if args:
            command.extend(args)

        def do_upload():
            try:
                result = self._run_command(command, "upload", needs_port=True)
                if self._on_complete:
                    self._on_complete(result)
            finally:
                self._set_state(BuildState.IDLE)

        if async_:
            self._build_thread = threading.Thread(target=do_upload, daemon=True)
            self._build_thread.start()
            return None
        else:
            try:
                return self._run_command(command, "upload", needs_port=True)
            finally:
                self._set_state(BuildState.IDLE)

    def run(
        self,
        args: Optional[list[str]] = None,
        environment: Optional[str] = None,
        async_: bool = True
    ) -> Optional[BuildResult]:
        """
        Build and upload in one operation.

        Args:
            args: Additional arguments
            environment: Build environment name
            async_: Run asynchronously

        Returns:
            BuildResult if synchronous, None if async
        """
        if self.is_busy:
            logger.warning("Build already in progress")
            return None

        def do_run():
            # Build first
            self._set_state(BuildState.BUILDING)
            build_cmd = self._detect_build_command(environment)
            if args:
                build_cmd.extend(args)

            build_result = self._run_command(build_cmd, "build", needs_port=False)

            if not build_result.success:
                self._set_state(BuildState.IDLE)
                if self._on_complete:
                    self._on_complete(build_result)
                return build_result

            # Then upload
            self._set_state(BuildState.UPLOADING)
            upload_cmd = self._detect_upload_command(environment)

            upload_result = self._run_command(upload_cmd, "upload", needs_port=True)

            self._set_state(BuildState.IDLE)
            if self._on_complete:
                self._on_complete(upload_result)
            return upload_result

        if async_:
            self._build_thread = threading.Thread(target=do_run, daemon=True)
            self._build_thread.start()
            return None
        else:
            return do_run()

    def cancel(self) -> bool:
        """
        Cancel the current build operation.

        Returns:
            True if a build was cancelled
        """
        if not self.is_busy:
            return False

        self._cancel_requested = True

        if self._current_process:
            try:
                self._current_process.terminate()
                # Give it a moment to terminate gracefully
                time.sleep(0.5)
                if self._current_process.poll() is None:
                    self._current_process.kill()
            except Exception as e:
                logger.error(f"Error cancelling build: {e}")

        return True

    def wait(self, timeout: Optional[float] = None) -> bool:
        """
        Wait for current build to complete.

        Args:
            timeout: Maximum seconds to wait

        Returns:
            True if build completed, False if timeout
        """
        if self._build_thread and self._build_thread.is_alive():
            self._build_thread.join(timeout=timeout)
            return not self._build_thread.is_alive()
        return True
