from __future__ import annotations

import json
import sys
from typing import Any, Mapping, Optional

from autotouch_cli.exceptions import AutotouchInputError

__all__ = [
    "OUTPUT_MODES",
    "normalize_output_mode",
    "normalize_output_projection",
    "print_json",
    "print_human",
    "emit_text",
    "apply_output_projection",
    "resolve_output_select_path",
    "resolve_json_pointer_value",
    "as_display",
    "supports_color",
]


OUTPUT_MODES = ("json", "ndjson", "human")


def normalize_output_mode(mode: Optional[str], *, default: str = "json") -> str:
    normalized = str(mode or default).strip().lower()
    return normalized if normalized in OUTPUT_MODES else default


def normalize_output_projection(select_expr: Optional[str], json_pointer: Optional[str]) -> tuple[Optional[str], Optional[str]]:
    return (
        str(select_expr or "").strip() or None,
        str(json_pointer or "").strip() or None,
    )


def supports_color(*, isatty: bool, environ: Mapping[str, str]) -> bool:
    return bool(isatty) and environ.get("NO_COLOR") is None


def as_display(value: Any) -> str:
    if value is None:
        return "-"
    if isinstance(value, bool):
        return "yes" if value else "no"
    if isinstance(value, (int, float, str)):
        return str(value)
    return json.dumps(value, default=str)


def resolve_output_select_path(value: Any, select_expr: str) -> Any:
    current = value
    for raw_part in [part for part in str(select_expr).split(".") if part != ""]:
        if isinstance(current, list):
            try:
                index = int(raw_part)
            except ValueError:
                raise AutotouchInputError(f"--select segment '{raw_part}' is not a valid list index")
            if index < 0 or index >= len(current):
                raise AutotouchInputError(f"--select index '{raw_part}' is out of range")
            current = current[index]
            continue
        if isinstance(current, dict):
            if raw_part not in current:
                raise AutotouchInputError(f"--select path '{select_expr}' not found")
            current = current[raw_part]
            continue
        raise AutotouchInputError(f"--select path '{select_expr}' cannot descend into {type(current).__name__}")
    return current


def resolve_json_pointer_value(value: Any, pointer: str) -> Any:
    if pointer == "":
        return value
    if not pointer.startswith("/"):
        raise AutotouchInputError("--json-pointer must start with '/'")
    current = value
    for raw_token in pointer.split("/")[1:]:
        token = raw_token.replace("~1", "/").replace("~0", "~")
        if isinstance(current, list):
            try:
                index = int(token)
            except ValueError:
                raise AutotouchInputError(f"--json-pointer token '{token}' is not a valid list index")
            if index < 0 or index >= len(current):
                raise AutotouchInputError(f"--json-pointer token '{token}' is out of range")
            current = current[index]
            continue
        if isinstance(current, dict):
            if token not in current:
                raise AutotouchInputError(f"--json-pointer path '{pointer}' not found")
            current = current[token]
            continue
        raise AutotouchInputError(f"--json-pointer path '{pointer}' cannot descend into {type(current).__name__}")
    return current


def apply_output_projection(data: Any, *, output_select: Optional[str], output_json_pointer: Optional[str]) -> Any:
    if output_json_pointer:
        data = resolve_json_pointer_value(data, output_json_pointer)
    if output_select:
        data = resolve_output_select_path(data, output_select)
    return data


def emit_text(text: str = "", *, file: Any = None) -> None:
    stream = file or sys.stdout
    try:
        print(text, file=stream)
    except BrokenPipeError:
        raise SystemExit(0)


def print_human(data: Any, *, file: Any = None) -> None:
    stream = file or sys.stdout
    if isinstance(data, dict):
        width = max((len(str(k)) for k in data.keys()), default=0)
        for key, value in data.items():
            emit_text(f"{str(key).ljust(width)} : {as_display(value)}", file=stream)
        return
    if isinstance(data, list):
        if not data:
            emit_text("No results.", file=stream)
            return
        if all(isinstance(item, dict) for item in data):
            for idx, item in enumerate(data, 1):
                title = item.get("name") or item.get("title") or item.get("id") or f"item-{idx}"
                emit_text(f"[{idx}] {title}", file=stream)
                width = max((len(str(k)) for k in item.keys()), default=0)
                for key, value in item.items():
                    emit_text(f"  {str(key).ljust(width)} : {as_display(value)}", file=stream)
                if idx < len(data):
                    emit_text("", file=stream)
            return
        for idx, item in enumerate(data, 1):
            emit_text(f"{idx}. {as_display(item)}", file=stream)
        return
    emit_text(as_display(data), file=stream)


def print_json(
    data: Any,
    *,
    compact: bool = False,
    file: Any = None,
    apply_projection: bool = True,
    output_mode: str = "json",
    output_select: Optional[str] = None,
    output_json_pointer: Optional[str] = None,
) -> None:
    stream = file or sys.stdout
    if apply_projection and (output_select or output_json_pointer):
        data = apply_output_projection_fn(
            data,
            output_select=output_select,
            output_json_pointer=output_json_pointer,
        )
    if output_mode == "human":
        print_human(data, file=stream)
        return
    if output_mode == "ndjson":
        emit_text(json.dumps(data, separators=(",", ":"), default=str), file=stream)
        return
    if compact:
        emit_text(json.dumps(data, separators=(",", ":"), default=str), file=stream)
        return
    emit_text(json.dumps(data, indent=2, default=str), file=stream)


# Alias avoids shadowing the apply_projection parameter above.
apply_output_projection_fn = apply_output_projection
