import logging
import random
import re
import unicodedata
import numpy as np
from supertonic import TTS
from .ident import ident

SENTENCE_END_RE = re.compile(r'[.!?]["\')\]]*(?=\s|$)')
BREAK_TAG_RE = re.compile(
    r"<break\s+"
    r"time\s*=\s*"
    r"(['\"])(?P<break_time>\d+(?:\.\d+)?)s\1"
    r"\s*/>",
    re.IGNORECASE,
)
Segment = tuple[str, str | dict | None]

TTS_CHAR_TRANSLATION = str.maketrans({
    "‘": "'",
    "’": "'",
    "‚": "'",
    "‛": "'",
    "“": '"',
    "”": '"',
    "„": '"',
    "‟": '"',
    "«": '"',
    "»": '"',
    "‹": "'",
    "›": "'",
    "–": "-",
    "—": "-",
    "−": "-",
    "…": "...",
    "\u00a0": " ",
    "\u200b": "",
    "\u200c": "",
    "\u200d": "",
    "\ufeff": "", 
    "\u00ad": ""  
})

logger = logging.getLogger(__name__)

class TTSEngine:
    def __init__(self):
        self._tts: TTS | None = None

    @property
    def tts(self) -> TTS:
        if self._tts is None:
            self._tts = TTS(
                model="supertonic-3",
                auto_download=True
            )

        return self._tts

    @property
    def sample_rate(self) -> int:
        return self.tts.sample_rate

    def text2speech(
        self,
        segments: list[Segment],
        params: dict,
    ) -> tuple[np.ndarray, float]:
        "Returns wav, duration in seconds"


        audio_parts = []
        total_duration = 0.0

        for segment in segments:
            wav, duration = self.render_segment(segment, params)

            audio_parts.append(as_mono_float_array(wav))
            total_duration += duration

        if not audio_parts:
            raise ValueError("Input text is empty")

        return np.concatenate(audio_parts), total_duration

    def render_segment(
        self,
        segment: Segment,
        params: dict,
    ) -> tuple[np.ndarray, float]:
        kind, value = segment

        if kind == "text":
            return self.render_text_segment(value, params)

        if kind == "break":
            return self.render_break_segment(value["seconds"])

        if kind == "ident":
            return self.render_ident_segment()

        raise ValueError(f"Unsupported audio segment: {segment!r}")

    def render_text_segment(
        self,
        text: str,
        params: dict,
    ) -> tuple[np.ndarray, float]:
        "Returns wav, duration in seconds"

        params = params.copy()
        text = sanitize_text(text)

        if not text.strip():
            raise ValueError("Input text is empty")

        voice_name = params.pop("voice_name")

        voice_style = self.tts.get_voice_style(
            voice_name=voice_name
        )


        wav, duration = self.tts.synthesize(
            text,
            voice_style=voice_style,
            **params
        )

        return wav, duration_to_seconds(duration)

    def render_break_segment(self, seconds: float) -> tuple[np.ndarray, float]:
        num_samples = int(seconds * self.sample_rate)
        return np.zeros(num_samples, dtype=np.float32), seconds

    def render_ident_segment(self) -> tuple[np.ndarray, float]:
        if ident.sample_rate != self.sample_rate:
            raise ValueError(
                f"Ident sample rate is {ident.sample_rate}, "
                f"but speech sample rate is {self.sample_rate}"
            )

        return ident.wav, ident.duration

    def prepare_segments(
        self,
        text: str,
        insert_ident: bool = False,
    ) -> list[Segment]:
        segments = parse_audio_segments(text)

        if insert_ident:
            segments = insert_ident_segment(segments)

        return segments

tts_engine: TTSEngine = TTSEngine()

def as_mono_float_array(wav: np.ndarray) -> np.ndarray:

    wav = np.asarray(wav, dtype=np.float32).squeeze()

    if wav.ndim != 1:
        raise ValueError(f"Expected mono audio, got shape {wav.shape}")

    return wav


def duration_to_seconds(duration) -> float:

    if hasattr(duration, "item"):
        return float(duration.item())

    return float(duration)

def sanitize_text(text):
    text = unicodedata.normalize("NFKC", text)
    text = text.translate(TTS_CHAR_TRANSLATION)
    return text.strip()

def parse_audio_segments(text: str) -> list[Segment]:
    segments = []
    start = 0

    for match in BREAK_TAG_RE.finditer(text):
        add_text_segment(segments, text[start:match.start()])
        seconds = float(match.group("break_time"))
        if seconds <= 0:
            raise ValueError("Break time must be greater than zero")
        segments.append(("break", {"seconds": seconds}))

        start = match.end()

    add_text_segment(segments, text[start:])
    return segments

def add_text_segment(segments: list[Segment], text: str) -> None:
    text = text.strip()
    if text:
        segments.append(("text", text))

def insert_ident_segment(segments: list[Segment]) -> list[Segment]:

    if not segments:
        return segments

    if len(segments) == 1:
        kind, value = segments[0]
        if kind == "text":
            return split_text_segment_for_ident(value)


    for index, segment in enumerate(segments):
        kind, _ = segment
        if kind == "break":
            return [
                *segments[:index],
                ("ident", None),
                *segments[index + 1:],
            ]


    return [
        *segments[:1],
        ("ident", None),
        *segments[1:],
    ]


def split_text_segment_for_ident(text: str) -> list[Segment]:
    part1, part2 = split_text_randomly(text)

    if not part1:
        return [("text", text)]

    if not part2:
        return [("text", part1), ("ident", None)]

    return [("text", part1), ("ident", None), ("text", part2)]


def split_text_randomly(text: str) -> tuple[str, str]:

    text = text.strip()

    if not text:
        return "", ""

    random_index = random.randint(1, len(text) - 1)
    split_index = find_sentence_end_after(text, random_index)

    if split_index is None:
        return text, ""

    part1 = text[:split_index].rstrip()
    part2 = text[split_index:].lstrip()
    return part1, part2

def find_sentence_end_after(text: str, start_index: int) -> int | None:

    match = SENTENCE_END_RE.search(text, start_index)

    if match and match.end() < len(text):
        return match.end()

    return None
