import ast
import os
import re
import time
import asyncio

from Core import settings as config

# ==================================================
# 🔤 Arabic & English normalization
# ==================================================

def _norm_ar(s: str) -> str:
    s = (s or "").strip().lower()
    if not s:
        return ""

    # Arabic punctuation: ؟ ، ؛
    s = re.sub(r"[\u061F\u060C\u061B]", " ", s)

    # ✅ Keep Arabic, English letters, word chars, and spaces
    s = re.sub(r"[^\w\s\u0600-\u06FFa-zA-Z]", " ", s)

    # collapse spaces
    s = re.sub(r"\s+", " ", s).strip()

    # normalize hamza/alif variants
    s = s.replace("أ", "ا").replace("إ", "ا").replace("آ", "ا")

    # ta marbuta -> ha, alif maqsoora -> ya
    s = s.replace("ة", "ه").replace("ى", "ي")

    # tatweel
    s = s.replace("ـ", "")

    # common nickname normalization
    s = s.replace("ابو", "بو")

    return s.strip()


# ==================================================
# 📂 Load wake phrases from a python file (sanad_arm.txt)
# Modified to search the local Scripts folder if not found next to module
# ==================================================

def load_arm_phrases(filename: str = "sanad_arm.txt", *, var_name: str = "WAKE_PHRASES") -> set[str]:
    base_dir = os.path.dirname(os.path.abspath(__file__))

    # Candidate locations (module dir, module dir / Scripts, parent / Scripts)
    candidates = [
        os.path.join(base_dir, filename),
        os.path.join(base_dir, "Scripts", filename),
        os.path.join(base_dir, "..", "Scripts", filename),
    ]

    arm_path = None
    for c in candidates:
        if os.path.exists(c):
            arm_path = c
            break

    if arm_path is None:
        raise FileNotFoundError(f"Arm phrases file not found in candidates: {candidates}")

    print(f"📂 Loading arm phrases from: {arm_path} (var={var_name})")

    with open(arm_path, "r", encoding="utf-8-sig") as f:
        raw = f.read()

    # Try to parse as python file (legacy format with WAKE_PHRASES variables).
    # Uses ast.parse + ast.literal_eval instead of exec() for safety.
    try:
        tree = ast.parse(raw)
        for node in ast.walk(tree):
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id == var_name:
                        phrases = ast.literal_eval(node.value)
                        if isinstance(phrases, (set, list, tuple)):
                            out = {_norm_ar(str(p)) for p in phrases if str(p).strip()}
                            out = {p for p in out if p}
                            print(f"✅ Loaded {len(out)} wake phrases from file variable: {var_name}")
                            return out
    except Exception:
        # fall through to plain text parsing
        pass

    # Plain-text newline-separated phrases fallback
    lines = [ln.strip() for ln in raw.splitlines()]
    cand = [ln for ln in lines if ln and not ln.startswith("#")]
    out = {_norm_ar(ln) for ln in cand}
    out = {p for p in out if p}
    print(f"✅ Loaded {len(out)} wake phrases from plain text file: {arm_path}")
    return out


# ==================================================
# 🧠 Wake phrase matching + scheduling
# ==================================================

def _is_valid_text(s: str) -> bool:
    # Allow Arabic OR English letters
    has_ar = bool(re.search(r"[\u0600-\u06FF]", s or ""))
    has_en = bool(re.search(r"[a-zA-Z]", s or ""))
    return has_ar or has_en


def _strip_ya_prefix(s: str) -> str:
    s = (s or "").strip()
    if not s:
        return ""
    if s.startswith("يا "):
        return s[3:].strip()
    if s.startswith("يا"):
        return s[2:].strip()
    return s


def _remove_al_prefix_words(text: str) -> str:
    if not text:
        return ""
    parts = text.split()
    out = []
    for w in parts:
        if w.startswith("ال") and len(w) > 2:
            out.append(w[2:])
        else:
            out.append(w)
    return " ".join(out).strip()


def _maybe_trigger_arm(
    obj,
    transcript_text: str,
    wake_phrases: set[str],
    *,
    fire_on_wake_match: bool = False,
    arm_trigger_fn=None,
) -> bool:
    if not transcript_text or not wake_phrases:
        return False

    # initialization of state variables (copied from original logic)
    if not hasattr(obj, "_asr_buf"):
        obj._asr_buf = ""
    if not hasattr(obj, "_asr_last_time"):
        obj._asr_last_time = 0.0
    if not hasattr(obj, "ASR_WINDOW_SEC"):
        obj.ASR_WINDOW_SEC = 2.0
    if not hasattr(obj, "ASR_SHORT_TOKEN_BONUS_SEC"):
        obj.ASR_SHORT_TOKEN_BONUS_SEC = 1.0
    if not hasattr(obj, "ASR_JOIN_NO_SPACE_MAXLEN"):
        obj.ASR_JOIN_NO_SPACE_MAXLEN = 2
    if not hasattr(obj, "ASR_MAX_CHARS"):
        obj.ASR_MAX_CHARS = 120

    if not hasattr(obj, "_last_trigger_norm"):
        obj._last_trigger_norm = ""
    if not hasattr(obj, "_last_trigger_time"):
        obj._last_trigger_time = 0.0
    if not hasattr(obj, "TRIGGER_DEDUP_WINDOW"):
        obj.TRIGGER_DEDUP_WINDOW = 2.0

    if not hasattr(obj, "_pending_arm_wave"):
        obj._pending_arm_wave = False
    if not hasattr(obj, "_pending_arm_wave_fired"):
        obj._pending_arm_wave_fired = False
    if not hasattr(obj, "_pending_arm_wave_set_time"):
        obj._pending_arm_wave_set_time = 0.0
    if not hasattr(obj, "PENDING_ARM_TTL"):
        obj.PENDING_ARM_TTL = 6.0

    if not hasattr(obj, "_pending_arm_trigger_fn"):
        obj._pending_arm_trigger_fn = None
    if not hasattr(obj, "_pending_arm_fallback_time"):
        obj._pending_arm_fallback_time = 0.0

    if not hasattr(obj, "_last_piece_call_norm"):
        obj._last_piece_call_norm = ""
    if not hasattr(obj, "_last_piece_call_time"):
        obj._last_piece_call_time = 0.0

    if not hasattr(obj, "_asr_stream"):
        obj._asr_stream = ""
    if not hasattr(obj, "ASR_STREAM_MAX_CHARS"):
        obj.ASR_STREAM_MAX_CHARS = 80

    dup_call_window = float(getattr(obj, "DUP_CALL_WINDOW_SEC", 0.25))
    dup_asr_repeat_window = float(getattr(obj, "DUP_ASR_REPEAT_WINDOW_SEC", 0.9))
    pending_fallback_sec = float(getattr(obj, "PENDING_ARM_FALLBACK_SEC", 0.65))

    piece_raw = (transcript_text or "").strip()
    if not piece_raw:
        return False

    piece_norm = _norm_ar(piece_raw)
    if not piece_norm:
        return False

    now = time.time()

    if not _is_valid_text(piece_norm):
        return False

    duplicate_call = (
        (piece_norm == obj._last_piece_call_norm)
        and ((now - obj._last_piece_call_time) < dup_call_window)
    )

    repeated_asr = (
        (piece_norm == obj._last_piece_call_norm)
        and ((now - obj._last_piece_call_time) < dup_asr_repeat_window)
    )

    obj._last_piece_call_norm = piece_norm
    obj._last_piece_call_time = now

    if not duplicate_call and not repeated_asr:
        print(f"📝 USER SAID (raw): {piece_raw}")
        print(f"📝 USER SAID (norm): {piece_norm}")

    if not duplicate_call and not repeated_asr:
        if obj._asr_last_time:
            gap = now - obj._asr_last_time
            window = obj.ASR_WINDOW_SEC
            if len(piece_norm) <= obj.ASR_JOIN_NO_SPACE_MAXLEN:
                window += obj.ASR_SHORT_TOKEN_BONUS_SEC
            if gap > window:
                obj._asr_buf = ""
                obj._asr_stream = ""

        obj._asr_last_time = now

        if obj._asr_buf:
            if len(piece_norm) <= obj.ASR_JOIN_NO_SPACE_MAXLEN:
                obj._asr_buf = (obj._asr_buf + piece_norm).strip()
            else:
                obj._asr_buf = (obj._asr_buf + " " + piece_norm).strip()
        else:
            obj._asr_buf = piece_norm

        compact = piece_norm.replace(" ", "")
        obj._asr_stream = (obj._asr_stream + compact)[-obj.ASR_STREAM_MAX_CHARS :]

        if len(obj._asr_buf) > obj.ASR_MAX_CHARS:
            obj._asr_buf = obj._asr_buf[-obj.ASR_MAX_CHARS :]

    buf_norm = _norm_ar(obj._asr_buf)
    buf_nospace = buf_norm.replace(" ", "")
    buf_noal = _remove_al_prefix_words(buf_norm)
    buf_noal_nospace = buf_noal.replace(" ", "")
    stream = _norm_ar(obj._asr_stream).replace(" ", "")
    stream_noal = _remove_al_prefix_words(stream)

    if not duplicate_call and not repeated_asr:
        print(f"🧩 ASR BUFFER: {buf_norm}")

    if (
        buf_norm == obj._last_trigger_norm
        and (now - obj._last_trigger_time) < obj.TRIGGER_DEDUP_WINDOW
    ):
        return False

    for phrase in wake_phrases:
        p_norm = _strip_ya_prefix(_norm_ar(str(phrase)))
        if not p_norm:
            continue

        p_nospace = p_norm.replace(" ", "")
        p_noal = _remove_al_prefix_words(p_norm)
        p_noal_nospace = p_noal.replace(" ", "")

        pattern = r'\b' + re.escape(p_norm) + r'\b'
        hit_buf = bool(re.search(pattern, buf_norm)) or (p_nospace and p_nospace == buf_nospace)
        hit_buf = hit_buf or (p_noal and (p_noal in buf_noal or (p_noal_nospace and p_noal_nospace in buf_noal_nospace)))

        hit_stream = False
        if p_nospace and p_nospace in stream:
            hit_stream = True
        elif p_noal_nospace and p_noal_nospace in stream_noal:
            hit_stream = True

        if hit_buf or hit_stream:
            if hit_stream and not hit_buf:
                print(f"⚡ FAST MATCH: '{phrase}' (recent tokens)")
            else:
                print(f"✅ MATCH: '{phrase}'  in  BUFFER='{obj._asr_buf}'")

            obj._last_trigger_norm = buf_norm
            obj._last_trigger_time = now

            obj._asr_buf = ""
            obj._asr_last_time = 0.0
            obj._asr_stream = ""

            if fire_on_wake_match:
                if arm_trigger_fn:
                    asyncio.create_task(asyncio.to_thread(arm_trigger_fn))
                obj._pending_arm_wave = False
                obj._pending_arm_wave_fired = False
                obj._pending_arm_wave_set_time = 0.0
                obj._pending_arm_trigger_fn = None
                obj._pending_arm_fallback_time = 0.0
            else:
                obj._pending_arm_wave = True
                obj._pending_arm_wave_fired = False
                obj._pending_arm_wave_set_time = now
                obj._pending_arm_trigger_fn = arm_trigger_fn
                obj._pending_arm_fallback_time = now + pending_fallback_sec

            return True

    return False


# ==================================================
# 🔁 Phrase map loader (plain-text grouped format)
# Format: groups separated by blank lines. First line = canonical command,
# following lines = aliases.
_phrase_map_cache = {}

def load_phrase_map(filename: str = "photo_command_ai.txt") -> dict:
    base_dir = os.path.dirname(os.path.abspath(__file__))
    candidates = [
        str(config.PHOTO_PHRASES_FILE),
        os.path.join(base_dir, filename),
        os.path.join(base_dir, "Scripts", filename),
        os.path.join(base_dir, "Data", filename),
        os.path.join(base_dir, "..", "Data", "Scripts", filename),
        os.path.join(base_dir, "..", "Scripts", filename),
        os.path.join(base_dir, "..", "Data", filename),
    ]
    path = None
    for c in candidates:
        if os.path.exists(c):
            path = c
            break
    if path is None:
        return {}

    if path in _phrase_map_cache:
        return _phrase_map_cache[path]

    with open(path, 'r', encoding='utf-8-sig') as f:
        lines = [ln.rstrip() for ln in f.readlines()]

    groups = []
    cur = []
    for ln in lines:
        s = ln.strip()
        if not s:
            if cur:
                groups.append(cur)
                cur = []
            continue
        if s.startswith("#"):
            continue
        cur.append(s)
    if cur:
        groups.append(cur)

    mapping = {}
    for g in groups:
        cmd = g[0]
        aliases = g
        for a in aliases:
            mapping[_norm_ar(a)] = cmd

    _phrase_map_cache[path] = mapping
    print(f"✅ Loaded phrase mapping from {path}: {len(mapping)} aliases")
    return mapping
