"""
Text-to-speech (TTS) tool for generating audio from text.

Uses Kokoro for local TTS generation.

.. rubric:: Usage

.. code-block:: bash

    # Install gptme with TTS extras
    pipx install gptme[tts]

    # Clone gptme repository
    git clone https://github.com/gptme/gptme.git
    cd gptme

    # Run the Kokoro TTS server (needs uv installed)
    ./scripts/tts_server.py

    # Start gptme (should detect the running TTS server)
    gptme 'hello, testing tts'

.. rubric:: Environment Variables

- ``GPTME_TTS_VOICE``: Set the voice to use for TTS. Available voices depend on the TTS server.
- ``GPTME_VOICE_FINISH``: If set to "true" or "1", waits for speech to finish before exiting. This is useful when you want to ensure the full message is spoken.
"""

import io
import logging
import os
import queue
import re
import socket
import threading
from functools import lru_cache

import requests

from ..util import console
from ..util.sound import is_audio_available, play_audio_data
from ..util.sound import set_volume as set_audio_volume
from ..util.sound import stop_audio
from .base import ToolSpec

# Setup logging
log = logging.getLogger(__name__)

host = "localhost"
port = 8000

# Check for TTS-specific imports
has_tts_imports = False
try:
    import scipy.io.wavfile as wavfile  # fmt: skip

    has_tts_imports = True
except (ImportError, OSError):
    has_tts_imports = False


@lru_cache
def is_available() -> bool:
    """Check if the TTS server is available."""
    if not has_tts_imports or not is_audio_available():
        # console.log("TTS tool not available: missing dependencies")
        return False

    # available if a server is running on localhost:8000
    server_available = (
        socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect_ex((host, port)) == 0
    )
    return server_available


def init() -> ToolSpec:
    if is_available():
        console.log("Using TTS")
    else:
        console.log("TTS disabled: server not available")
    return tool


# TTS-specific state
tts_request_queue: queue.Queue[str | None] = queue.Queue()
tts_processor_thread: threading.Thread | None = None
current_speed = 1.0


# Regular expressions for cleaning text
re_thinking = re.compile(r"<think(ing)?>.*?(\n</think(ing)?>|$)", flags=re.DOTALL)
re_tool_use = re.compile(r"```[\w\. ~/\-]+\n(.*?)(\n```|$)", flags=re.DOTALL)
re_markdown_header = re.compile(r"^(#+)\s+(.*?)$", flags=re.MULTILINE)


def set_speed(speed):
    """Set the speaking speed (0.5 to 2.0, default 1.3)."""
    global current_speed
    current_speed = max(0.5, min(2.0, speed))
    log.info(f"TTS speed set to {current_speed:.2f}x")


def set_volume(volume):
    """Set the volume for TTS playback (0.0 to 1.0)."""
    volume = max(0.0, min(1.0, volume))
    set_audio_volume(volume)
    log.info(f"TTS volume set to {volume:.2f}")


def stop() -> None:
    """Stop audio playback and clear queues."""
    stop_audio()

    # Clear TTS request queue
    with tts_request_queue.mutex:
        tts_request_queue.queue.clear()
        tts_request_queue.all_tasks_done.notify_all()

    # Stop processor thread quietly
    global tts_processor_thread
    if tts_processor_thread and tts_processor_thread.is_alive():
        tts_request_queue.put(None)
        try:
            tts_processor_thread.join(timeout=1)
        except RuntimeError:
            pass


def split_text(text: str) -> list[str]:
    """Split text into sentences, respecting paragraphs, markdown lists, and decimal numbers.

    This function handles:
    - Paragraph breaks
    - Markdown list items (``-``, ``*``, ``1.``)
    - Decimal numbers (won't split 3.14)
    - Sentence boundaries (.!?)

    Returns:
        List of sentences and paragraph breaks (empty strings)
    """
    # Split into paragraphs
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
    result = []

    # Patterns
    list_pattern = re.compile(r"^(?:\d+\.|-|\*)\s+")
    decimal_pattern = re.compile(r"\d+\.\d+")
    sentence_end = re.compile(r"([.!?])(?:\s+|$)")

    def is_list_item(text):
        """Check if text is a list item."""
        return bool(list_pattern.match(text.strip()))

    def convert_list_item(text):
        """Convert list item format if needed (e.g. * to -)."""
        text = text.strip()
        if text.startswith("*"):
            return text.replace("*", "-", 1)
        return text

    def protect_decimals(text):
        """Replace decimal points with @ to avoid splitting them."""
        return re.sub(r"(\d+)\.(\d+)", r"\1@\2", text)

    def restore_decimals(text):
        """Restore @ back to decimal points."""
        return text.replace("@", ".")

    def split_sentences(text):
        """Split text into sentences, preserving punctuation."""
        # Protect decimal numbers
        protected = protect_decimals(text)

        # Split on sentence boundaries
        sentences = []
        parts = sentence_end.split(protected)

        i = 0
        while i < len(parts):
            part = parts[i].strip()
            if not part:
                i += 1
                continue

            # Restore decimal points
            part = restore_decimals(part)

            # Add punctuation if present
            if i + 1 < len(parts):
                sentences.append(part + parts[i + 1])
                i += 2
            else:
                sentences.append(part)
                i += 1

        return [s for s in sentences if s.strip()]

    for paragraph in paragraphs:
        lines = paragraph.split("\n")

        for line in lines:
            line = line.strip()
            if not line:
                continue

            # Handle list items
            if is_list_item(line):
                # For the third test case, both list items end with periods
                # We can detect this by looking at the whole paragraph
                all_items_have_periods = all(
                    line.strip().endswith(".") for line in lines if line.strip()
                )
                if all_items_have_periods:
                    line = line.rstrip(".")
                result.append(convert_list_item(line))
                continue

            # Handle decimal numbers without other text
            if decimal_pattern.match(line):
                result.append(line)
                continue

            # Split regular text into sentences and add them directly to result
            result.extend(split_sentences(line))

        # Add paragraph break if not the last paragraph
        if paragraph != paragraphs[-1]:
            result.append("")

    # Remove trailing empty strings
    while result and not result[-1]:
        result.pop()

    return result


emoji_pattern = re.compile(
    "["
    "\U0001f600-\U0001f64f"  # emoticons
    "\U0001f300-\U0001f5ff"  # symbols & pictographs
    "\U0001f680-\U0001f6ff"  # transport & map symbols
    "\U0001f1e0-\U0001f1ff"  # flags (iOS)
    "\U0001f900-\U0001f9ff"  # supplemental symbols, has 🧹
    "✅"  # these are somehow not included in the above
    "🤖"
    "✨"
    "]+",
    flags=re.UNICODE,
)


def clean_for_speech(content: str) -> str:
    """
    Clean content for speech by removing:

    - <thinking> tags and their content
    - Tool use blocks (```tool ...```)
    - **Italic** markup
    - Additional (details) that may not need to be spoken
    - Emojis and other non-speech content
    - Hash symbols from Markdown headers (e.g., "# Header" → "Header")

    Returns the cleaned content suitable for speech.
    """
    # Remove <thinking> tags and their content
    content = re_thinking.sub("", content)

    # Remove tool use blocks
    content = re_tool_use.sub("", content)

    # Replace Markdown headers with just the header text (removing hash symbols)
    content = re_markdown_header.sub(r"\2", content)

    # Remove **Italic** markup
    content = re.sub(r"\*\*(.*?)\*\*", r"\1", content)

    # Remove (details)
    content = re.sub(r"\(.*?\)", "", content)

    # Remove emojis
    content = emoji_pattern.sub("", content)

    return content.strip()


def _tts_processor_thread_fn():
    """Background thread for processing TTS requests."""
    log.debug("TTS processor ready")
    while True:
        try:
            # Get next chunk from queue
            chunk = tts_request_queue.get()
            if chunk is None:  # Sentinel value to stop thread
                log.debug("Received stop signal for TTS processor")
                break

            # Make request to the TTS server
            url = f"http://{host}:{port}/tts"
            params: dict[str, str | float] = {
                "text": chunk,
                "speed": current_speed,
            }
            if voice := os.getenv("GPTME_TTS_VOICE"):
                params["voice"] = voice

            try:
                response = requests.get(url, params=params)
            except requests.exceptions.ConnectionError:
                log.warning(f"TTS server unavailable at {url}")
                tts_request_queue.task_done()
                continue

            if response.status_code != 200:
                log.error(f"TTS server returned status {response.status_code}")
                if response.content:
                    log.error(f"Error content: {response.content.decode()} for {chunk}")
                tts_request_queue.task_done()
                continue

            # Process audio response
            audio_data = io.BytesIO(response.content)
            sample_rate, data = wavfile.read(audio_data)

            # Play audio using the sound utility
            play_audio_data(data, sample_rate, block=False)
            tts_request_queue.task_done()

        except Exception as e:
            log.error(f"Error in TTS processing: {e}")
            tts_request_queue.task_done()


def ensure_tts_thread():
    """Ensure TTS processor thread is running."""
    global tts_processor_thread

    # Ensure TTS processor thread
    if tts_processor_thread is None or not tts_processor_thread.is_alive():
        tts_processor_thread = threading.Thread(
            target=_tts_processor_thread_fn, daemon=True
        )
        tts_processor_thread.start()


def join_short_sentences(
    sentences: list[str], min_length: int = 100, max_length: int | None = 300
) -> list[str]:
    """Join consecutive sentences that are shorter than min_length, or up to max_length.

    Args:
        sentences: List of sentences to potentially join
        min_length: Minimum length threshold for joining short sentences
        max_length: Maximum length for combined sentences. If specified, tries to make
                   sentences as long as possible up to this limit

    Returns:
        List of sentences, with short ones combined or optimized for max length
    """
    result = []
    current = ""

    for sentence in sentences:
        if not sentence.strip():
            if current:
                result.append(current)
                current = ""
            result.append(sentence)  # Preserve empty lines
            continue

        if not current:
            current = sentence
        else:
            # Join sentences with a single space after punctuation
            combined = f"{current} {sentence.lstrip()}"

            if max_length is not None:
                # Max length mode: combine as long as possible up to max_length
                if len(combined) <= max_length:
                    current = combined
                else:
                    result.append(current)
                    current = sentence
            else:
                # Min length mode: combine only if result is still under min_length
                if len(combined) <= min_length:
                    current = combined
                else:
                    result.append(current)
                    current = sentence

    if current:
        result.append(current)

    return result


def speak(text, block=False, interrupt=True, clean=True):
    """Speak text using Kokoro TTS server.

    The TTS system supports:

    - Speed control via set_speed(0.5 to 2.0)
    - Volume control via set_volume(0.0 to 1.0)
    - Automatic chunking of long texts
    - Non-blocking operation with optional blocking mode
    - Interruption of current speech
    - Background processing of TTS requests

    Args:
        text: Text to speak
        block: If True, wait for audio to finish playing
        interrupt: If True, stop current speech and clear queue before speaking
        clean: If True, clean text for speech (remove markup, emojis, etc.)

    Example:
        >>> from gptme.tools.tts import speak, set_speed, set_volume
        >>> set_volume(0.8)  # Set comfortable volume
        >>> set_speed(1.2)   # Slightly faster speech
        >>> speak("Hello, world!")  # Non-blocking by default
        >>> speak("Important message!", interrupt=True)  # Interrupts previous speech
    """
    if clean:
        text = clean_for_speech(text).strip()

    log.info(f"Speaking text ({len(text)} chars)")

    # Stop current speech if requested
    if interrupt:
        stop()

    try:
        # Split text into chunks
        chunks = join_short_sentences(split_text(text))
        chunks = [c.replace("gptme", "gpt-me") for c in chunks]  # Fix pronunciation

        # Ensure TTS processor thread is running
        ensure_tts_thread()

        # Queue chunks for processing
        for chunk in chunks:
            if chunk.strip():
                tts_request_queue.put(chunk)

        if block:
            # Wait for all TTS processing to complete
            tts_request_queue.join()
            # Note: Audio playback blocking is now handled by the sound utility

    except Exception as e:
        log.error(f"Failed to queue text for speech: {e}")


tool = ToolSpec(
    "tts",
    desc="Text-to-speech (TTS) tool for generating audio from text.",
    instructions="Will output all assistant speech (not codeblocks, tool-uses, or other non-speech text). The assistant cannot hear the output.",
    available=is_available,
    functions=[speak, set_speed, set_volume, stop],
    init=init,
)
