from __future__ import annotations

import contextlib
import json
import os
import re
import shutil
import sys
import textwrap
from collections.abc import Iterable, Sequence
from enum import Enum
from typing import IO, Any, Generic, Iterator, TypeVar
from uuid import UUID

from pydantic import BaseModel
from radicli import HelpFormatter
from radicli.util import format_arg_help
from wasabi import msg
from wasabi.tables import table
from wasabi.util import color

from prodigy_teams_pam_sdk.models import RecipeDetail

from .about import __prog__
from .messages import Messages
from .ty import Page
from .util import CYGWIN, WIN

LOGO = """
█▀█ █▀█ █▀█ █▀▄ █ █▀▀ █▄█   ▀█▀ █▀▀ ▄▀█ █▀▄▀█ █▀
█▀▀ █▀▄ █▄█ █▄▀ █ █▄█ ░█░   ░█░ ██▄ █▀█ █░▀░█ ▄█
"""

whitespace_re = re.compile(r"\s+", re.ASCII)
ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")


def is_silent() -> bool:
    # External environment variable to disable printing of info tables etc.
    return bool(os.environ.get("PRODIGY_TEAMS_CLI_SILENT", False))


def print_login_info(
    uri_complete: str, uri: str, code: str, no_browser: bool = False
) -> None:
    if not isatty(sys.stdout):
        print_as_json(
            {"action": "login_required", "uri": uri_complete, "user_code": code}
        )
        return
    print(LOGO)
    msg.info(Messages.T014)
    if not no_browser:
        open_url(uri_complete)
    msg.text(Messages.T015)
    msg.text(color(uri_complete, bold=True))
    print("")
    msg.text(Messages.T016)
    msg.text(color(uri, bold=True))
    msg.text(Messages.T017)
    msg.text(color(code, bold=True))


def print_as_json(data: dict[str, Any]) -> None:
    if not is_silent():
        print(json.dumps(data, indent=2, default=str))


def print_logs(text: str | None, as_json: bool = False) -> None:
    if is_silent():
        return
    if as_json:
        print_as_json({"logs": text})
    elif text is not None:
        msg.info("Logs")
        print(text)


def dicts_to_table(
    data: Sequence[dict[str, Any]],
    headers: list[str] | None = None,
    exclude_dicts: bool = False,
) -> tuple[list[str], Sequence[dict[str, Any]] | list[list[str]]]:
    """
    Generate a tuple with headers and rows in the format wasabi expects from a list
    of dicts having column names as keys.
    """
    if not data:
        return [], data
    use_data = list(data)
    if headers is None:
        headers = list(use_data[0].keys())
    rows = []
    for item in data:
        if exclude_dicts:
            item = {k: v for k, v in item.items() if not isinstance(v, dict)}
        row = []
        for key in headers:
            val = item.get(key, None)
            if isinstance(val, Enum):
                val = val.value
            row.append(str(val))
        rows.append(row)
    return headers, rows


def print_table_with_select(
    items: Sequence[BaseModel | dict],  # type: ignore[type-arg]
    select: list[str] | None = None,
    as_json: bool = False,
) -> None:
    if is_silent():
        return
    include: set[str] | None = set(select) if select is not None else None

    def to_dict(r: Any) -> dict[str, Any]:
        if isinstance(r, BaseModel):
            d = r.model_dump(include=include)
        elif hasattr(r, "as_dict"):
            d = r.as_dict(include=include)
        else:
            d = r
        if include:
            return {k: v for k, v in d.items() if k in include}
        return d

    if as_json:
        print(json.dumps([to_dict(item) for item in items], default=str))
    else:
        data = [to_dict(r) for r in items]
        headers, rows = dicts_to_table(data, headers=select)
        if rows:
            msg.table(rows, header=headers, divider=True, max_col=3000)


def print_info_table(
    item: BaseModel,
    select: list[str] | None = None,
    exclude: list[str] | None = None,
    as_json: bool = False,
) -> None:
    if is_silent():
        return
    dict_kwargs: dict[str, Any] = {}
    if select is not None:
        dict_kwargs["include"] = set(select)
    if exclude is not None:
        dict_kwargs["exclude"] = set(exclude)
    if as_json:
        print(item.model_dump_json(**dict_kwargs, indent=2))
    else:
        serialized = {}
        for key, val in item.model_dump(**dict_kwargs).items():
            if isinstance(val, Enum):
                serialized[key] = val.value
            else:
                serialized[key] = val
        msg.table(serialized)


def print_args_table(
    args: dict[str, Any], cli_names: dict[str, str] | None = None
) -> None:
    if is_silent():
        return
    data = {}
    cli_names_map = {v: k for k, v in cli_names.items()} if cli_names else {}
    for key, value in args.items():
        key = cli_names_map.get(key, key)
        value = str(value) if isinstance(value, UUID) else repr(value)
        data[key] = value
    msg.table(data)


def print_recipes_help(
    schemas: dict[str, RecipeDetail], help: str, command: str
) -> None:
    data = []
    for name, schema in schemas.items():
        data.append((name, schema.title))
        if schema.description:
            data.append(("", format_arg_help(schema.description)))
    print(f"usage: {__prog__} {command} [-h] name [...]\n")
    print(
        f"{help}\nRun {__prog__} {command} [name] --help to view the recipe arguments."
    )
    print("")
    msg.text("Available recipes:")
    msg.table(data)


def get_recipe_desc(schema: RecipeDetail) -> str:
    width = max(shutil.get_terminal_size().columns - 2, 11)
    text = whitespace_re.sub(" ", schema.description or "").strip()
    desc = textwrap.fill(text, width, initial_indent="", subsequent_indent="")
    meta = {
        "ID": schema.id,
        "Source": schema.entry_point,
        "Created": schema.created.strftime("%Y-%m-%d, %H:%M:%S"),
    }
    meta_table = table(meta)
    return f"{schema.title} ({schema.name})\n{desc}\n{meta_table}"


class RecipeHelpFormatter(HelpFormatter):
    def _fill_text(self, text, width, indent) -> str:
        return "".join(indent + line for line in text.splitlines(keepends=True))


# Source: https://github.com/pallets/click/blob/cba52fa76135af2edf46c154203b47106f898eb3/src/click/_termui_impl.py#L551
def open_url(url: str, wait: bool = False, locate: bool = False) -> int:
    import subprocess

    def _unquote_file(url: str) -> str:
        from urllib.parse import unquote

        if url.startswith("file://"):
            url = unquote(url[7:])

        return url

    if sys.platform == "darwin":
        args = ["open"]
        if wait:
            args.append("-W")
        if locate:
            args.append("-R")
        args.append(_unquote_file(url))
        null = open("/dev/null", "w")
        try:
            return subprocess.Popen(args, stderr=null).wait()
        finally:
            null.close()
    elif WIN:
        if locate:
            url = _unquote_file(url.replace('"', ""))
            args = f'explorer /select,"{url}"'
        else:
            url = url.replace('"', "")
            wait_str = "/WAIT" if wait else ""
            args = f'start {wait_str} "" "{url}"'
        return os.system(args)
    elif CYGWIN:
        if locate:
            url = os.path.dirname(_unquote_file(url).replace('"', ""))
            args = f'cygstart "{url}"'
        else:
            url = url.replace('"', "")
            wait_str = "-w" if wait else ""
            args = f'cygstart {wait_str} "{url}"'
        return os.system(args)
    try:
        if locate:
            url = os.path.dirname(_unquote_file(url)) or "."
        else:
            url = _unquote_file(url)
        c = subprocess.Popen(
            ["xdg-open", url],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        if wait:
            return c.wait()
        return 0
    except OSError:
        if url.startswith(("http://", "https://")) and not locate and not wait:
            import webbrowser

            webbrowser.open(url)
            return 0
        return 1


# Adapted from: https://github.com/pallets/click/blob/cba52fa76135af2edf46c154203b47106f898eb3/src/click/_termui_impl.py#L35
# Only supports sized sequences and basic percent / count
_BarT = TypeVar("_BarT")


class Progress(Generic[_BarT]):
    show_pc: bool
    file: IO[str]
    _completed_intervals: int
    width: int
    before_bar: str
    after_bar: str
    iter: Iterator[_BarT]
    length: int
    pos: int
    finished: bool
    max_width: int | None
    current_item: _BarT | None
    is_hidden: bool
    _last_line: str | None

    def __init__(
        self,
        iterable: Iterable[_BarT] | Sequence[_BarT],
        length: int | None = None,
        pc: bool = False,
        width: int = 36,
    ) -> None:
        self.show_pc = pc
        self.file = sys.stdout
        self._completed_intervals = 0
        self.width = width
        self.before_bar = "\r" if os.name == "nt" else "\r\033[?25l"
        self.after_bar = "\n" if os.name == "nt" else "\033[?25h\n"
        self.iter = iter(iterable)
        self.length = (
            length
            if length is not None
            else len(iterable)
            if isinstance(iterable, Sequence)
            else 0
        )
        self.pos = 0
        self.finished = False
        self.max_width = None
        self.current_item = None
        self.is_hidden = not isatty(self.file)
        self._last_line = None

    def __enter__(self) -> Progress[_BarT]:
        self.render_progress()
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        tb: Any,
    ) -> None:
        self.render_finish()

    def __iter__(self) -> Iterator[_BarT]:
        self.render_progress()
        return self.generator()

    def __next__(self) -> _BarT:
        return next(iter(self))

    def render_finish(self) -> None:
        if self.is_hidden:
            return
        self.file.write(self.after_bar)
        self.file.flush()

    @property
    def pct(self) -> float:
        if self.length == 0:
            return 1.0
        return 1.0 if self.finished else min(self.pos / float(self.length), 1.0)

    def format_bar(self) -> str:
        empty_char = " "
        fill_char = "#"
        if self.finished:
            return fill_char * self.width
        bar_length = int(self.pct * self.width)
        bar = fill_char * bar_length
        bar += empty_char * (self.width - bar_length)
        return bar

    def render_progress(self) -> None:
        if self.is_hidden:
            return
        buf = []
        clear_width = self.width
        if self.max_width is not None:
            clear_width = self.max_width
        buf.append(self.before_bar)
        info = (
            f"{int(self.pct * 100): 4}%"[1:]
            if self.show_pc
            else f"{str(self.pos)}/{self.length}"
        )
        line = f"[{self.format_bar()}]  {info}".rstrip()
        line_len = len(ansi_re.sub("", line))
        if self.max_width is None or self.max_width < line_len:
            self.max_width = line_len
        buf.append(line)
        buf.append(" " * (clear_width - line_len))
        line = "".join(buf)
        if line != self._last_line:
            self._last_line = line
            self.file.write(line)
            self.file.flush()

    def make_step(self, n_steps: int) -> None:
        self.pos += n_steps
        if self.pos >= self.length:
            self.finished = True

    def update(self, n_steps: int, current_item: _BarT | None = None) -> None:
        if current_item is not None:
            self.current_item = current_item
        self._completed_intervals += n_steps
        if self._completed_intervals >= 1:
            self.make_step(self._completed_intervals)
            self.render_progress()
            self._completed_intervals = 0

    def finish(self) -> None:
        self.current_item = None
        self.finished = True

    def generator(self) -> Iterator[_BarT]:
        if self.is_hidden:
            yield from self.iter
        else:
            for rv in self.iter:
                self.current_item = rv
                if self._completed_intervals == 0:
                    self.render_progress()
                yield rv
                self.update(1)
            self.finish()
            self.render_progress()


def isatty(stream: IO[Any]) -> bool:
    if not hasattr(stream, "isatty"):
        return False
    try:
        return stream.isatty()
    except ValueError:
        return False


@contextlib.contextmanager
def loading(text: str = "Loading..."):
    """TTY-aware loading spinner. Suppresses animation in non-TTY."""
    if isatty(sys.stdout):
        with msg.loading(text):
            yield
    else:
        yield


def print_mutation_result(
    result: dict[str, Any],
    message: str,
    detail: str | None = None,
    *,
    as_json: bool = False,
) -> None:
    if is_silent():
        return
    if as_json:
        print(json.dumps(result, indent=2, default=str))
    else:
        msg.good(message, detail or "")


class PageProgress(Progress[Page]):
    _pos: int

    def __init__(
        self, iterable: Iterable[Page], *args: Any, length: int = 1, **kwargs: Any
    ) -> None:
        super().__init__(iterable, *args, length=length, **kwargs)
        self._pos = 0

    def generator(self) -> Iterator[Page]:
        for page in super().generator():
            self.length = page.total
            self._pos += len(page.items)
            self.pos = self._pos - 1

            yield page
