import asyncio
import json
import re
from datetime import datetime, timedelta, timezone
from typing import Optional

import httpx
import typer
from arcadepy import Arcade, NotFoundError
from arcadepy.types import WorkerHealthResponse, WorkerResponse
from dateutil import parser
from rich.console import Console
from rich.table import Table

from arcade_cli.constants import (
    PROD_ENGINE_HOST,
)
from arcade_cli.usage.command_tracker import TrackedTyper, TrackedTyperGroup
from arcade_cli.utils import (
    compute_base_url,
    handle_cli_error,
    validate_and_get_config,
)

console = Console()


def _format_timestamp_to_local(timestamp_str: str) -> str:
    """
    Convert a UTC timestamp string to local timezone format.

    Args:
        timestamp_str: UTC timestamp in format "2025-10-22T21:08:23.508906574Z"

    Returns:
        Formatted timestamp string in local timezone
    """
    try:
        # Parse the UTC timestamp and convert to local timezone
        utc_dt = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
        local_dt = utc_dt.astimezone()
        return local_dt.strftime("%Y-%m-%d %H:%M:%S %Z")
    except (ValueError, TypeError):
        # If parsing fails, return the original timestamp
        return timestamp_str


def _parse_time_string(time_str: str) -> datetime:
    """
    Parse a time string that can be either relative (e.g., "1h", "42m", "2d")
    or absolute (e.g., "2025-10-03T12:24:36Z").

    Args:
        time_str: Time string in relative or absolute format

    Returns:
        datetime object in UTC timezone

    Raises:
        ValueError: If the time string cannot be parsed
    """
    if not time_str:
        raise ValueError("Time string cannot be empty")

    # Handle relative time formats (e.g., "1h", "42m", "2d", "0m")
    relative_pattern = r"^(\d+)([smhd])$"
    match = re.match(relative_pattern, time_str.lower())

    if match:
        value = int(match.group(1))
        unit = match.group(2)

        now = datetime.now(timezone.utc)

        if unit == "s":
            delta = timedelta(seconds=value)
        elif unit == "m":
            delta = timedelta(minutes=value)
        elif unit == "h":
            delta = timedelta(hours=value)
        elif unit == "d":
            delta = timedelta(days=value)
        else:
            raise ValueError(f"Unsupported time unit: {unit}")

        return now - delta

    # Handle absolute time formats using dateutil parser
    try:
        parsed_dt = parser.parse(time_str)

        # Ensure timezone awareness
        if parsed_dt.tzinfo is None:
            # Assume UTC if no timezone info
            parsed_dt = parsed_dt.replace(tzinfo=timezone.utc)
        else:
            # Convert to UTC
            parsed_dt = parsed_dt.astimezone(timezone.utc)
    except (ValueError, TypeError) as e:
        raise ValueError(f"Unable to parse time string '{time_str}': {e}")

    return parsed_dt


app = TrackedTyper(
    cls=TrackedTyperGroup,
    add_completion=False,
    no_args_is_help=True,
    pretty_exceptions_enable=False,
    pretty_exceptions_show_locals=False,
    pretty_exceptions_short=True,
)

state = {
    "engine_url": compute_base_url(
        host=PROD_ENGINE_HOST, port=None, force_tls=False, force_no_tls=False
    )
}


@app.callback()
def main(
    host: str = typer.Option(
        PROD_ENGINE_HOST,
        "--host",
        "-h",
        help="The Arcade Engine host.",
    ),
    port: int = typer.Option(
        None,
        "--port",
        "-p",
        help="The port of the Arcade Engine host.",
    ),
    force_tls: bool = typer.Option(
        False,
        "--tls",
        help="Whether to force TLS for the connection to the Arcade Engine.",
    ),
    force_no_tls: bool = typer.Option(
        False,
        "--no-tls",
        help="Whether to disable TLS for the connection to the Arcade Engine.",
    ),
) -> None:
    """
    Manage users in the system.
    """
    engine_url = compute_base_url(force_tls, force_no_tls, host, port)
    state["engine_url"] = engine_url


@app.command("list", help="List all servers")
def list_servers(
    debug: bool = typer.Option(
        False,
        "--debug",
        "-d",
        help="Show debug information",
    ),
) -> None:
    config = validate_and_get_config()
    base_url = state["engine_url"]
    client = Arcade(api_key=config.api.key, base_url=base_url)
    try:
        servers = client.workers.list(limit=100)
        _print_servers_table(servers.items)
    except Exception as e:
        handle_cli_error("Failed to list servers", e, debug=debug)


@app.command("get", help="Get a server's details")
def get_server(
    server_name: str,
    debug: bool = typer.Option(
        False,
        "--debug",
        "-d",
        help="Show debug information",
    ),
) -> None:
    config = validate_and_get_config()
    base_url = state["engine_url"]
    client = Arcade(api_key=config.api.key, base_url=base_url)
    try:
        server = client.workers.get(server_name)
        server_health = client.workers.health(server_name)
        _print_server_details(server, server_health)
    except Exception as e:
        handle_cli_error(f"Failed to get server '{server_name}'", e, debug=debug)


@app.command("enable", help="Enable a server")
def enable_server(
    server_name: str,
    debug: bool = typer.Option(
        False,
        "--debug",
        "-d",
        help="Show debug information",
    ),
) -> None:
    config = validate_and_get_config()
    engine_url = state["engine_url"]
    arcade = Arcade(api_key=config.api.key, base_url=engine_url)
    try:
        arcade.workers.update(server_name, enabled=True)
    except Exception as e:
        handle_cli_error(f"Failed to enable worker '{server_name}'", e, debug=debug)


@app.command("disable", help="Disable a server")
def disable_server(
    server_name: str,
    debug: bool = typer.Option(
        False,
        "--debug",
        "-d",
        help="Show debug information",
    ),
) -> None:
    config = validate_and_get_config()
    engine_url = state["engine_url"]
    arcade = Arcade(api_key=config.api.key, base_url=engine_url)
    try:
        arcade.workers.update(server_name, enabled=False)
    except Exception as e:
        handle_cli_error(f"Failed to disable worker '{server_name}'", e, debug=debug)


@app.command("delete", help="Delete a server that is managed by Arcade")
def delete_server(
    server_name: str,
    debug: bool = typer.Option(
        False,
        "--debug",
        "-d",
        help="Show debug information",
    ),
) -> None:
    config = validate_and_get_config()
    engine_url = state["engine_url"]

    try:
        arcade = Arcade(api_key=config.api.key, base_url=engine_url)
        arcade.workers.delete(server_name)
        console.print(f"✓ Server '{server_name}' deleted successfully", style="green")
    except NotFoundError as e:
        handle_cli_error(
            f"Server '{server_name}' doesn't exist or cannot be deleted", e, debug=debug
        )
    except Exception as e:
        handle_cli_error(
            f"Server '{server_name}' doesn't exist or cannot be deleted", e, debug=debug
        )


@app.command("logs", help="Get logs for a server that is managed by Arcade")
def get_server_logs(
    server_name: str,
    follow: bool = typer.Option(
        False,
        "--follow",
        "-f",
        is_flag=True,
        help="Follow (stream) the log output in real-time",
        rich_help_panel="Streaming Options",
    ),
    since: Optional[str] = typer.Option(
        None,
        "--since",
        "-s",
        help="Show logs since timestamp (e.g., 2025-10-03T12:24:36Z) or relative (e.g., 42m for 42 minutes ago). Defaults to 1h (1 hour ago) for non-streaming, 0s (now) for streaming.",
        rich_help_panel="Time Range Options",
    ),
    until: Optional[str] = typer.Option(
        None,
        "--until",
        "-u",
        help="Show logs until timestamp (e.g., 2025-10-03T12:24:36Z) or relative (e.g., 42m for 42 minutes ago). Defaults to 0s (now).",
        rich_help_panel="Time Range Options",
    ),
    debug: bool = typer.Option(
        False,
        "--debug",
        "-d",
        help="Show debug information",
    ),
) -> None:
    config = validate_and_get_config()
    headers = {"Authorization": f"Bearer {config.api.key}", "Content-Type": "application/json"}

    # Set defaults based on whether we're following or not
    if since is None:
        since = "0s" if follow else "1h"
    if until is None:
        until = "0s"

    try:
        # Parse time strings to UTC datetime objects
        since_dt = _parse_time_string(since)
        until_dt = _parse_time_string(until)

        # Validate that since is before until
        if since_dt >= until_dt:
            raise ValueError(f"'since' time ({since}) must be before 'until' time ({until})")  # noqa: TRY301
    except ValueError as e:
        handle_cli_error(f"Invalid time format: {e}", debug=debug)

    if follow:
        # Use the streaming endpoint
        engine_url = state["engine_url"] + f"/v1/deployments/{server_name}/logs/stream"
        asyncio.run(_stream_deployment_logs(engine_url, headers, since_dt, until_dt, debug=debug))
    else:
        # Use the non-streaming endpoint
        engine_url = state["engine_url"] + f"/v1/deployments/{server_name}/logs"
        _display_deployment_logs(engine_url, headers, since_dt, until_dt, debug=debug)


def _display_deployment_logs(
    engine_url: str, headers: dict, since: datetime, until: datetime, debug: bool
) -> None:
    try:
        with httpx.Client() as client:
            params = {"start_time_utc": since.isoformat(), "end_time_utc": until.isoformat()}
            response = client.get(engine_url, headers=headers, params=params)
            response.raise_for_status()
            logs = response.json()
            for log in logs:
                formatted_timestamp = _format_timestamp_to_local(log["timestamp"])
                print(f"[{formatted_timestamp}] {log['line']}")
    except httpx.HTTPStatusError as e:
        handle_cli_error(
            f"Failed to fetch logs: {e.response.status_code} {e.response.text}", debug=debug
        )
    except Exception as e:
        handle_cli_error(f"Error fetching logs: {e}", debug=debug)


async def _stream_deployment_logs(
    engine_url: str, headers: dict, since: datetime, until: datetime, debug: bool
) -> None:
    try:
        async with (
            httpx.AsyncClient(timeout=None) as client,  # noqa: S113 - expected indefinite log stream
            client.stream(
                "GET",
                engine_url,
                headers=headers,
                params={"start_time_utc": since.isoformat(), "end_time_utc": until.isoformat()},
            ) as response,
        ):
            response.raise_for_status()
            async for line in response.aiter_lines():
                if not line:
                    continue

                # Handle SSE format: "data: {json}"
                if line.startswith("data: "):
                    try:
                        data = json.loads(line[6:])
                        timestamp_str = data.get("Timestamp", "")
                        log_line = data.get("Line", "")
                        formatted_timestamp = _format_timestamp_to_local(timestamp_str)
                        print(f"[{formatted_timestamp}] {log_line}")
                    except (json.JSONDecodeError, KeyError, IndexError):
                        print(line)
                else:
                    print(line)
    except httpx.HTTPStatusError as e:
        handle_cli_error(f"Failed to stream logs: {e.response.status_code}", debug=debug)
    except Exception as e:
        handle_cli_error(f"Error streaming logs: {e}", debug=debug)


def _print_servers_table(servers: list[WorkerResponse]) -> None:
    if not servers:
        console.print("No servers found", style="bold red")
        return

    table = Table(title="Servers")
    table.add_column("Name")
    table.add_column("Enabled")
    table.add_column("Host")
    table.add_column("Managed by Arcade")

    for server in servers:
        if server.id is None:
            continue
        uri = server.http.uri if server.http and server.http.uri else "N/A"
        table.add_row(
            server.id,
            str(server.enabled),
            uri,
            str(server.managed),
        )
    console.print(table)


def _print_server_details(server: WorkerResponse, server_health: WorkerHealthResponse) -> None:
    table = Table(title="Server Details")
    table.add_column("Name")
    table.add_column("Enabled")
    table.add_column("Is Healthy")
    table.add_column("Host")
    table.add_column("Managed by Arcade")
    uri = server.http.uri if server.http and server.http.uri else "N/A"
    table.add_row(
        server.id, str(server.enabled), str(server_health.healthy), uri, str(server.managed)
    )
    console.print(table)
