import functools
import io
import logging
import random
import re
import shutil
import sys
import termios
import textwrap
from datetime import datetime, timedelta
from functools import lru_cache
from pathlib import Path
from typing import Any

import tiktoken
from rich import print
from rich.console import Console
from rich.syntax import Syntax

from .clipboard import copy, set_copytext

EMOJI_WARN = "⚠️"

logger = logging.getLogger(__name__)

console = Console(log_path=False)


def get_tokenizer(model: str):
    if "gpt-4" in model or "gpt-3.5" in model:
        return tiktoken.encoding_for_model(model)
    else:  # pragma: no cover
        logger.warning(
            f"No encoder implemented for model {model}."
            "Defaulting to tiktoken cl100k_base encoder."
            "Use results only as estimates."
        )
        return tiktoken.get_encoding("cl100k_base")


actions = [
    "running",
    "jumping",
    "walking",
    "skipping",
    "hopping",
    "flying",
    "swimming",
    "crawling",
    "sneaking",
    "sprinting",
    "sneaking",
    "dancing",
    "singing",
    "laughing",
]
adjectives = [
    "funny",
    "happy",
    "sad",
    "angry",
    "silly",
    "crazy",
    "sneaky",
    "sleepy",
    "hungry",
    # colors
    "red",
    "blue",
    "green",
    "pink",
    "purple",
    "yellow",
    "orange",
]
nouns = [
    "cat",
    "dog",
    "rat",
    "mouse",
    "fish",
    "elephant",
    "dinosaur",
    # birds
    "bird",
    "pelican",
    # fictional
    "dragon",
    "unicorn",
    "mermaid",
    "monster",
    "alien",
    "robot",
    # sea creatures
    "whale",
    "shark",
    "walrus",
    "octopus",
    "squid",
    "jellyfish",
    "starfish",
    "penguin",
    "seal",
]


def generate_name():
    action = random.choice(actions)
    adjective = random.choice(adjectives)
    noun = random.choice(nouns)
    return f"{action}-{adjective}-{noun}"


def is_generated_name(name: str) -> bool:
    """if name is a name generated by generate_name"""
    all_words = actions + adjectives + nouns
    return name.count("-") == 2 and all(word in all_words for word in name.split("-"))


def epoch_to_age(epoch, incl_date=False):
    # takes epoch and returns "x minutes ago", "3 hours ago", "yesterday", etc.
    age = datetime.now() - datetime.fromtimestamp(epoch)
    if age < timedelta(minutes=1):
        return "just now"
    elif age < timedelta(hours=1):
        return f"{age.seconds // 60} minutes ago"
    elif age < timedelta(days=1):
        return f"{age.seconds // 3600} hours ago"
    elif age < timedelta(days=2):
        return "yesterday"
    else:
        return f"{age.days} days ago" + (
            " ({datetime.fromtimestamp(epoch).strftime('%Y-%m-%d')})"
            if incl_date
            else ""
        )


copiable = False


def set_copiable():
    global copiable
    copiable = True


def clear_copiable():
    global copiable
    copiable = False


def print_preview(code: str, lang: str, copy: bool = False):  # pragma: no cover
    print()
    print("[bold white]Preview[/bold white]")

    if copy:
        set_copiable()
        set_copytext(code)

    # NOTE: we can set background_color="default" to remove background
    print(Syntax(code.strip("\n"), lang))
    print()


override_auto = False


def ask_execute(question="Execute code?", default=True) -> bool:  # pragma: no cover
    global override_auto
    if override_auto:
        return True

    print_bell()  # Ring the bell just before asking for input
    termios.tcflush(sys.stdin, termios.TCIFLUSH)  # flush stdin

    choicestr = f"[{'Y' if default else 'y'}/{'n' if default else 'N'}{'/c' if copiable else ''}/?]"
    answer = console.input(
        f"[bold bright_yellow on red] {question} {choicestr} [/] ",
    )

    if not override_auto and copiable and "c" == answer.lower().strip():
        if copy():
            print("Copied to clipboard.")
            return False
        clear_copiable()

    # secret option to stop asking for the rest of the session
    if answer.lower() in ["auto"]:
        return (override_auto := True)

    # secret option to ask for help
    if answer.lower() in ["help", "h", "?"]:
        lines = [
            "Options:",
            " y - execute the code",
            " n - do not execute the code",
            (" c - copy the code to the clipboard\n" if copiable else ""),
            " auto - stop asking for the rest of the session",
            f"Default is '{'y' if default else 'n'}' if answer is empty.",
        ]
        helptext = "\n".join(line for line in lines if line)
        print(helptext)
        return ask_execute(question, default)

    return answer.lower() in (["y", "yes"] + [""] if default else [])


def clean_example(s: str, strict=False) -> str:
    orig = s
    s = re.sub(
        r"(^|\n)([>] )?([A-Za-z]+):",
        r"\1\3:",
        s,
    )
    if strict:
        assert s != orig, "Couldn't find a message"
    return s


def example_to_xml(s: str) -> str:
    """
    Transforms an example with "> Role:" dividers into XML format <role>message</role>.
    """
    s = clean_example(s)
    orig = s
    print(f"After clean_example: {s!r}")  # Debug print

    lines = s.split("\n")
    result = []
    current_role = None
    current_message = []

    for line in lines:
        role_match = re.match(r"([A-Za-z]+):\s*(.*)", line)
        if role_match:
            if current_role and current_message:
                # Close previous role block
                result.append(
                    f"<{current_role}>\n"
                    + "\n".join(current_message)
                    + f"\n</{current_role}>"
                )
                current_message = []
            current_role = role_match.group(1).lower()
            current_message.append(role_match.group(2))
        else:
            if current_role:
                if line.strip() == "":
                    # Blank line indicates end of message
                    result.append(
                        f"<{current_role}>\n"
                        + "\n".join(current_message)
                        + f"\n</{current_role}>\n"
                    )
                    current_role = None
                    current_message = []
                else:
                    current_message.append(line)
            else:
                result.append(line)

    # Close any remaining role block
    if current_role and current_message:
        result.append(
            f"<{current_role}>\n"
            + "\n".join(current_message)
            + f"\n</{current_role}>\n"
        )

    s = "\n".join(result).strip()
    print(f"Final result: {s!r}")  # Debug print
    assert s != orig, "Couldn't find place to put start of directive"
    return s


def transform_examples_to_chat_directives(s: str, strict=False) -> str:
    """
    Transforms an example with "> Role:" dividers into ".. chat::" directive.
    """
    s = clean_example(s, strict=strict)
    s = textwrap.indent(s, "   ")
    orig = s
    s = re.sub(
        r"(^|\n)(   [# ]+(.+)(\n\s*)+)?   User:",
        r"\1\3\n\n.. chat::\n\n   User:",
        s,
    )
    if strict:
        assert s != orig, "Couldn't find place to put start of directive"
    return s


def print_bell():
    """Ring the terminal bell."""
    sys.stdout.write("\a")
    sys.stdout.flush()


@lru_cache
def _is_sphinx_build() -> bool:
    """Check if the code is being executed in a Sphinx build."""
    try:
        # noreorder
        import sphinx  # fmt: skip

        is_sphinx = hasattr(sphinx, "application")
    except ImportError:
        is_sphinx = False
    # print(f"Is Sphinx build: {is_sphinx}")
    return is_sphinx


def document_prompt_function(*args, **kwargs):
    """Decorator for adding example output of prompts to docstrings in rst format"""

    def decorator(func):  # pragma: no cover
        # only do the __doc__ decoration if in a Sphinx build
        if not _is_sphinx_build():
            return func

        # noreorder
        from .message import len_tokens  # fmt: skip
        from .tools import init_tools  # fmt: skip

        init_tools()

        prompt = "\n\n".join([msg.content for msg in func(*args, **kwargs)])
        prompt = textwrap.indent(prompt, "   ")
        prompt_tokens = len_tokens(prompt)
        kwargs_str = (
            (" (" + ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) + ")")
            if kwargs
            else ""
        )
        # unindent
        func.__doc__ = textwrap.dedent(func.__doc__ or "")
        func.__doc__ = func.__doc__.strip()
        func.__doc__ += f"\n\nExample output{kwargs_str}:"
        func.__doc__ += f"\n\n.. code-block:: markdown\n\n{prompt}"
        func.__doc__ += f"\n\nTokens: {prompt_tokens}"
        return func

    return decorator


def rich_to_str(s: Any, **kwargs) -> str:
    c = Console(file=io.StringIO(), **kwargs)
    c.print(s)
    return c.file.getvalue()  # type: ignore


def path_with_tilde(path: Path) -> str:
    home = str(Path.home())
    path_str = str(path)
    if path_str.startswith(home):
        return path_str.replace(home, "~", 1)
    return path_str


@functools.lru_cache
def get_installed_programs(candidates: tuple[str, ...]) -> set[str]:
    installed = set()
    for candidate in candidates:
        if shutil.which(candidate) is not None:
            installed.add(candidate)
    return installed
