from __future__ import annotations

import os
import socket
import subprocess
import sys
import time
from pathlib import Path
from typing import Any

import requests
import typer

from daisy_sdk.auth import get_valid_access_token
from daisy_sdk.constants import (
    CONTROL_INTERNAL_HOST,
    DEFAULT_APP_PORT,
    DEFAULT_CONTAINER_NAME,
    DEFAULT_CONTROL_BIND,
    DEFAULT_CONTROL_HOST,
    DEFAULT_CONTROL_PORT,
)
from daisy_sdk.container import container_running, describe_container
from daisy_sdk.image import fetch_image, find_existing_image
from daisy_sdk.utils import DEFAULT_PROJECT_CONFIG_PATH, DEFAULT_TOKEN_PATH, write_toml

app = typer.Typer(add_completion=False, invoke_without_command=True)


@app.callback(invoke_without_command=True)
def main(ctx: typer.Context) -> None:
    """Daisy CLI entrypoint."""
    if ctx.invoked_subcommand is None:
        typer.echo(ctx.get_help())


def _health_url(host: str, port: int) -> str:
    return f"http://{host}:{port}/health"


def _restart_url(host: str, port: int) -> str:
    return f"http://{host}:{port}/restart"


def _shutdown_url(host: str, port: int) -> str:
    return f"http://{host}:{port}/shutdown"


def check_health(control_host: str, control_port: int) -> dict[str, Any] | None:
    try:
        resp = requests.get(_health_url(control_host, control_port), timeout=2)
        if resp.ok:
            return resp.json()
    except requests.RequestException:
        return None
    return None


def app_port_from_container(info: dict[str, Any]) -> str | None:
    """Extract host port from docker ps JSON blob."""
    ports = info.get("Ports")
    if not ports:
        return None
    # Ports looks like "0.0.0.0:8080->80/tcp"
    if isinstance(ports, str):
        parts = ports.split(",")
        for part in parts:
            if "->80" in part:
                left = part.split("->")[0]
                host_port = left.split(":")[-1]
                if host_port:
                    return host_port
    return None


def ensure_host_docker_internal() -> None:
    """Ensure host.docker.internal resolves on the host (for browser -> control server)."""
    try:
        socket.getaddrinfo("host.docker.internal", None)
        return
    except OSError:
        pass

    hosts_path = Path("/etc/hosts")
    entry = "host.docker.internal"
    line = "127.0.0.1 host.docker.internal\n"

    # If already present but unresolved (odd), do nothing.
    if hosts_path.exists():
        try:
            content = hosts_path.read_text()
            if entry in content:
                return
        except Exception:
            pass

    if os.access(hosts_path, os.W_OK):
        try:
            with hosts_path.open("a") as handle:
                handle.write(line)
            typer.echo("Added host.docker.internal to /etc/hosts")
            return
        except Exception:
            pass

    typer.echo(
        "host.docker.internal is not resolvable from the host. "
        "Please add '127.0.0.1 host.docker.internal' to /etc/hosts "
        "(sudo sh -c \"echo '127.0.0.1 host.docker.internal' >> /etc/hosts\")."
    )


def start_server_process(
    *,
    image: str,
    app_port: str,
    control_host: str,
    control_bind: str,
    control_port: int,
    project_config_path: Path,
    platform: str,
    container_name: str,
    container_logs: bool,
    show_docker_command: bool,
) -> subprocess.Popen[str]:
    cmd = [
        sys.executable,
        "-m",
        "daisy_sdk.server",
        "--host",
        control_bind,
        "--port",
        str(control_port),
        "--image",
        image,
        "--app-port",
        app_port,
        "--project-config-path",
        str(project_config_path),
        "--platform",
        platform,
        "--container-name",
        container_name,
        "--container-logs" if container_logs else "",
        "--show-docker-command" if show_docker_command else "",
    ]
    return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)


def ensure_control_server(
    *,
    image: str,
    app_port: str,
    control_host: str,
    control_bind: str,
    control_port: int,
    project_config_path: Path,
    platform: str,
    container_name: str,
    detach_control: bool,
    container_logs: bool,
    show_docker_command: bool,
) -> None:
    client_host = "127.0.0.1" if control_host == "0.0.0.0" else control_host
    health = check_health(client_host, control_port)
    if health is None:
        typer.echo("Starting Daisy control server...")
        if detach_control:
            start_server_process(
                image=image,
                app_port=app_port,
                control_host=client_host,
                control_bind=control_bind,
                control_port=control_port,
                project_config_path=project_config_path,
                platform=platform,
                container_name=container_name,
                container_logs=container_logs,
                show_docker_command=show_docker_command,
            )
            # Wait for it to come up
            for _ in range(20):
                time.sleep(0.25)
                health = check_health(client_host, control_port)
                if health:
                    break
        else:
            # Run in foreground; this call blocks until server exits.
            from daisy_sdk.server import run_server, stop_container

            try:
                run_server(
                    bind_host=control_bind,
                    client_host=client_host,
                    port=control_port,
                    image=image,
                    app_port=app_port,
                    project_config_path=project_config_path,
                    platform=platform,
                    container_name=container_name,
                    detach=False,
                    log_container=container_logs,
                    show_docker_command=show_docker_command,
                )
            except KeyboardInterrupt:
                try:
                    stop_container(container_name)
                except Exception:
                    pass
                raise
            return
    if health is None:
        raise typer.Exit(code=1)

    # If image mismatch or container not running, request restart with desired image/port.
    if (health.get("image") != image) or (not health.get("running")):
        try:
            requests.post(
                _restart_url(client_host, control_port),
                json={"image": image, "port": app_port},
                timeout=5,
            )
        except requests.RequestException:
            raise typer.Exit(code=1)

    try:
        time.sleep(2)
        webbrowser.open(f"http://localhost:{app_port}")
    except Exception:
        pass


@app.command()
def stop(
    control_host: str = typer.Option(
        DEFAULT_CONTROL_HOST,
        "--control-host",
        help="Host interface the CLI will use to reach the control server.",
    ),
    control_bind: str = typer.Option(
        DEFAULT_CONTROL_BIND,
        "--control-bind",
        help="Interface the control server binds to (use 0.0.0.0 for container access).",
    ),
    control_port: int = typer.Option(
        DEFAULT_CONTROL_PORT,
        "--control-port",
        help="Port for the Daisy control server.",
    ),
    container_name: str = typer.Option(
        DEFAULT_CONTAINER_NAME,
        "--container-name",
        help="Name of the Daisy docker container to stop.",
    ),
) -> None:
    """Stop the Daisy container and control server."""
    try:
        resp = requests.post(_shutdown_url(control_host, control_port), timeout=3)
        if resp.ok:
            typer.echo("Sent shutdown to control server.")
        else:
            typer.echo(f"Control server responded with {resp.status_code}, attempting docker stop...")
    except requests.RequestException:
        typer.echo("Control server not reachable, attempting docker stop directly...")

    try:
        subprocess.run(["docker", "rm", "-f", container_name], check=False, capture_output=True)
        typer.echo(f"Stopped container {container_name}.")
    except FileNotFoundError:
        typer.echo("Docker not available; nothing else to do.")


@app.command()
def run(
    update: bool = typer.Option(
        False,
        "--update",
        help="Fetch the latest image even if one already exists locally.",
    ),
    port: str = typer.Option(
        DEFAULT_APP_PORT,
        "--port",
        help="Host port to expose the Daisy UI on.",
    ),
    control_host: str = typer.Option(
        DEFAULT_CONTROL_HOST,
        "--control-host",
        help="Host interface for the Daisy control server.",
    ),
    control_bind: str = typer.Option(
        DEFAULT_CONTROL_BIND,
        "--control-bind",
        help="Interface the control server binds to (use 0.0.0.0 for container access).",
    ),
    control_port: int = typer.Option(
        DEFAULT_CONTROL_PORT,
        "--control-port",
        help="Port for the Daisy control server.",
    ),
    platform: str | None = typer.Option(
        None,
        "--platform",
        help="Platform to pass to docker run (e.g., linux/arm64).",
    ),
    project_config_path: Path = typer.Option(
        DEFAULT_PROJECT_CONFIG_PATH,
        "--project-config-path",
        help="Path to projects.toml for mounting Daisy projects.",
    ),
    container_name: str = typer.Option(
        DEFAULT_CONTAINER_NAME,
        "--container-name",
        help="Name to give the Daisy docker container.",
    ),
    detach_control: bool = typer.Option(
        False,
        "--detach-control",
        help="Run the control server in the background instead of foreground (default: foreground).",
    ),
    image: str | None = typer.Option(
        None,
        "--image",
        help="Explicit image tag to run (skip fetch/discovery).",
    ),
    container_logs: bool = typer.Option(
        False,
        "--container-logs",
        help="Stream container logs to stdout.",
    ),
    show_docker_command: bool = typer.Option(
        False,
        "--show-docker-command",
        help="Print the docker run command used to start the container.",
    ),
) -> None:
    """Fetch (if needed) and run the Daisy image."""
    if control_port != DEFAULT_CONTROL_PORT:
        typer.echo(
            "Warning: The Daisy image expects host API at port "
            f"{DEFAULT_CONTROL_PORT}. Using a different port requires rebuilding the image with a new VITE_HOST_API_BASE."
        )
    ensure_host_docker_internal()
    chosen_image = image
    if chosen_image:
        typer.echo(f"Using provided image: {chosen_image}")
    else:
        if not update:
            typer.echo("Checking for existing Daisy image...")
            existing = find_existing_image()
            if existing:
                chosen_image = existing
                typer.echo(f"Using existing image: {existing}")
        if chosen_image is None:
            access_token = get_valid_access_token()
            typer.echo("Fetching Daisy image...")
            chosen_image = fetch_image(access_token)
        typer.echo(f"Selected image: {chosen_image}")

    # If container already running, just point the user to the port and exit.
    if container_running(container_name):
        info = describe_container(container_name)
        port_hint = app_port_from_container(info) or DEFAULT_APP_PORT
        typer.echo(
            f"Daisy container '{container_name}' already running. Visit http://localhost:{port_hint}"
        )
        return

    typer.echo(f"Daisy will be available at http://localhost:{port}")
    ensure_control_server(
        image=chosen_image,
        app_port=port,
        control_host=control_host,
        control_bind=control_bind,
        control_port=control_port,
        project_config_path=project_config_path,
        platform=platform,
        detach_control=detach_control,
        container_logs=container_logs,
        show_docker_command=show_docker_command,
        container_name=container_name,
    )
    if detach_control:
        typer.echo(
            f"Daisy running at http://localhost:{port} (control: http://{control_host}:{control_port})"
        )


@app.command()
def setup() -> None:
    """Set up the Daisy config directory and config file. Start here for first-time users."""
    created_paths: list[Path] = []

    config_path = DEFAULT_TOKEN_PATH
    if not config_path.parent.exists():
        config_path.parent.mkdir(parents=True, exist_ok=True)
        created_paths.append(config_path.parent)

    if not config_path.exists():
        write_toml(config_path, {})
        created_paths.append(config_path)

    if created_paths:
        typer.echo("\nDaisy setup complete.\n")
        typer.echo(f"Config file: {config_path}")
        typer.echo(
            "\nNext steps:\n"
            "- Complete the Daisy signup flow to get the keys for config.toml.\n"
            "- If you are not enrolled in the beta, email support@daisyhq.com.\n"
        )
    else:
        typer.echo("Daisy setup already complete.")
