"""Transduck — AI-native translation tool."""

import hashlib
import logging
import threading

from transduck.config import TransduckConfig, load_config
from transduck.hooks import TranslationEvent, _fire_hooks
from transduck.result import TranslationResult
from transduck.storage import TranslationStore
from transduck.validation import validate_translation, extract_placeholders

logger = logging.getLogger("transduck")


class _State:
    def __init__(self):
        self.config: TransduckConfig | None = None
        self.store: TranslationStore | None = None
        self.shared_store = None  # SharedStore | None — lazy import
        self.target_lang: str | None = None
        self._locks: dict[tuple, threading.Lock] = {}
        self._locks_lock = threading.Lock()
        self._inflight: set[tuple] = set()
        self._inflight_lock = threading.Lock()
        self._bg_done = threading.Event()
        self._bg_done.set()  # starts signaled (no background work)
        self._hooks: dict[str, list] = {}


_state = _State()


def _hash(text: str) -> str:
    return hashlib.sha256(text.encode()).hexdigest()


def _interpolate(text: str, vars: dict | None) -> str:
    if not vars:
        return text
    result = text
    for key, value in vars.items():
        result = result.replace(f"{{{key}}}", str(value))
    return result


def initialize(config: TransduckConfig | None = None):
    if config is None:
        config = load_config()
    _state.config = config
    _state.store = TranslationStore(config.storage_path)
    _state.store.initialize()

    # Initialize shared Postgres store if configured
    if config.shared_url:
        from transduck.shared_store import SharedStore
        _state.shared_store = SharedStore(config.shared_url)
        _state.shared_store.initialize()


def set_language(lang: str):
    _state.target_lang = lang.upper()


def on(event_name: str, callback) -> None:
    """Register a global hook. Example: transduck.on("translate", my_logger)"""
    _state._hooks.setdefault(event_name, []).append(callback)


def off(event_name: str, callback) -> None:
    """Unregister a global hook."""
    hooks = _state._hooks.get(event_name)
    if hooks:
        try:
            hooks.remove(callback)
        except ValueError:
            pass


def _wait_for_background(timeout: float = 5.0) -> bool:
    """Wait for all background translations to finish. Returns True if all completed."""
    return _state._bg_done.wait(timeout=timeout)


def _make_result(text: str, *, pending: bool, source_lang: str, lang: str,
                 vars: dict | None, source: str | None = None) -> TranslationResult:
    """Interpolate and wrap in TranslationResult."""
    interpolated = _interpolate(text, vars)
    return TranslationResult(interpolated, pending=pending, source_lang=source_lang,
                             lang=lang, source=source)


def _maybe_fire_hooks(*, source_text: str, translated_text: str, source_lang: str,
                      lang: str, context: str | None, source: str,
                      on_translate=None, plural: bool = False,
                      plural_category: str | None = None,
                      plural_forms: dict | None = None) -> None:
    """Fire hooks if any are registered. No-op otherwise."""
    global_hooks = _state._hooks.get("translate")
    if not global_hooks and on_translate is None:
        return
    cfg = _state.config
    event = TranslationEvent(
        source_text=source_text,
        translated_text=translated_text,
        source_lang=source_lang,
        lang=lang,
        context=context,
        project_context=cfg.project_context if cfg else "",
        source=source,
        plural=plural,
        plural_category=plural_category,
        plural_forms=plural_forms,
    )
    _fire_hooks(global_hooks or [], event, on_translate)


def ait(
    source_text: str,
    context: str | None = None,
    vars: dict | None = None,
    source_lang: str | None = None,
    background: bool = False,
    on_translate=None,
) -> TranslationResult:
    if _state.config is None or _state.store is None:
        raise RuntimeError("transduck not initialized. Call transduck.initialize() first.")
    if _state.target_lang is None:
        raise RuntimeError("Target language not set. Call transduck.set_language() first.")

    cfg = _state.config
    src_lang = (source_lang or cfg.source_lang).upper()
    tgt_lang = _state.target_lang

    if tgt_lang == src_lang:
        return _make_result(source_text, pending=False, source_lang=src_lang,
                            lang=tgt_lang, vars=vars)

    project_context_hash = _hash(cfg.project_context)
    string_context_hash = _hash(context or "")

    # Tier 1: local SQLite lookup
    cached = _state.store.lookup(
        source_text=source_text,
        source_lang=src_lang,
        target_lang=tgt_lang,
        project_context_hash=project_context_hash,
        string_context_hash=string_context_hash,
    )
    if cached is not None:
        _maybe_fire_hooks(source_text=source_text, translated_text=cached,
                          source_lang=src_lang, lang=tgt_lang, context=context,
                          source="cache", on_translate=on_translate)
        return _make_result(cached, pending=False, source_lang=src_lang,
                            lang=tgt_lang, vars=vars, source="cache")

    # Tier 2: shared Postgres lookup (if configured)
    if _state.shared_store is not None:
        shared_cached = _state.shared_store.lookup(
            source_text=source_text,
            source_lang=src_lang,
            target_lang=tgt_lang,
            project_context_hash=project_context_hash,
            string_context_hash=string_context_hash,
        )
        if shared_cached is not None:
            # Propagate to local store
            _state.store.insert(
                source_text=source_text,
                source_lang=src_lang,
                target_lang=tgt_lang,
                project_context_hash=project_context_hash,
                string_context_hash=string_context_hash,
                translated_text=shared_cached,
                model=cfg.backend_model,
                string_context=context or "",
            )
            _maybe_fire_hooks(source_text=source_text, translated_text=shared_cached,
                              source_lang=src_lang, lang=tgt_lang, context=context,
                              source="shared_cache", on_translate=on_translate)
            return _make_result(shared_cached, pending=False, source_lang=src_lang,
                                lang=tgt_lang, vars=vars, source="shared_cache")

    # Read-only mode: skip backend, return source text
    if cfg.read_only:
        logger.debug("Read-only mode, cache miss for: %s", source_text)
        return _make_result(source_text, pending=False, source_lang=src_lang,
                            lang=tgt_lang, vars=vars)

    # Background mode: fire-and-forget translation
    if background:
        content_hash = (source_text, src_lang, tgt_lang, project_context_hash, string_context_hash)
        with _state._inflight_lock:
            if content_hash in _state._inflight:
                return _make_result(source_text, pending=True, source_lang=src_lang,
                                    lang=tgt_lang, vars=vars)
            _state._inflight.add(content_hash)
            _state._bg_done.clear()

        def _bg_translate():
            try:
                translated = _do_translate(source_text, src_lang, tgt_lang, project_context_hash,
                                           string_context_hash, context, cfg)
                if translated is not None:
                    _maybe_fire_hooks(source_text=source_text, translated_text=translated,
                                      source_lang=src_lang, lang=tgt_lang, context=context,
                                      source="translated", on_translate=on_translate)
            finally:
                with _state._inflight_lock:
                    _state._inflight.discard(content_hash)
                    if not _state._inflight:
                        _state._bg_done.set()

        threading.Thread(target=_bg_translate, daemon=True).start()
        return _make_result(source_text, pending=True, source_lang=src_lang,
                            lang=tgt_lang, vars=vars)

    # Synchronous translation
    translated = _do_translate(source_text, src_lang, tgt_lang, project_context_hash,
                               string_context_hash, context, cfg)
    if translated is not None:
        _maybe_fire_hooks(source_text=source_text, translated_text=translated,
                          source_lang=src_lang, lang=tgt_lang, context=context,
                          source="translated", on_translate=on_translate)
        return _make_result(translated, pending=False, source_lang=src_lang,
                            lang=tgt_lang, vars=vars, source="translated")

    return _make_result(source_text, pending=False, source_lang=src_lang,
                        lang=tgt_lang, vars=vars)


def _do_translate(source_text, src_lang, tgt_lang, project_context_hash,
                  string_context_hash, context, cfg):
    """Perform translation with locking, backend call, validation, and storage.

    Returns translated text on success, or None on failure.
    """
    # In-process dedup lock
    lock_key = (source_text, src_lang, tgt_lang, project_context_hash, string_context_hash)
    with _state._locks_lock:
        if lock_key not in _state._locks:
            _state._locks[lock_key] = threading.Lock()
        lock = _state._locks[lock_key]

    with lock:
        # Double-check after acquiring lock
        cached = _state.store.lookup(
            source_text=source_text,
            source_lang=src_lang,
            target_lang=tgt_lang,
            project_context_hash=project_context_hash,
            string_context_hash=string_context_hash,
        )
        if cached is not None:
            return cached

        # Call backend (lazy import — openai is heavy)
        from transduck import backend
        try:
            translated = backend.translate(
                source_text=source_text,
                source_lang=src_lang,
                target_lang=tgt_lang,
                project_context=cfg.project_context,
                string_context=context,
                config=cfg,
            )
        except Exception:
            logger.warning("Backend failed for: %s", source_text, exc_info=True)
            return None

        # Validate
        if not validate_translation(source_text, translated):
            logger.warning("Validation failed for: %s -> %s", source_text, translated)
            _insert_both(
                source_text=source_text, source_lang=src_lang, target_lang=tgt_lang,
                project_context_hash=project_context_hash,
                string_context_hash=string_context_hash,
                translated_text=translated, model=cfg.backend_model,
                status="failed", string_context=context or "",
            )
            return None

        # Store and return
        _insert_both(
            source_text=source_text, source_lang=src_lang, target_lang=tgt_lang,
            project_context_hash=project_context_hash,
            string_context_hash=string_context_hash,
            translated_text=translated, model=cfg.backend_model,
            string_context=context or "",
        )
        return translated


def _insert_both(*, source_text, source_lang, target_lang, project_context_hash,
                 string_context_hash, translated_text, model, status="translated",
                 string_context=""):
    """Insert into local store and shared store (if configured)."""
    _state.store.insert(
        source_text=source_text, source_lang=source_lang, target_lang=target_lang,
        project_context_hash=project_context_hash,
        string_context_hash=string_context_hash,
        translated_text=translated_text, model=model, status=status,
        string_context=string_context,
    )
    if _state.shared_store is not None:
        try:
            _state.shared_store.insert(
                source_text=source_text, source_lang=source_lang, target_lang=target_lang,
                project_context_hash=project_context_hash,
                string_context_hash=string_context_hash,
                translated_text=translated_text, model=model, status=status,
                string_context=string_context,
            )
        except Exception:
            logger.warning("Failed to write to shared store", exc_info=True)


def _insert_plural_both(*, source_text, source_lang, target_lang, project_context_hash,
                        string_context_hash, plural_category, translated_text, model,
                        status="translated", string_context=""):
    """Insert plural into local store and shared store (if configured)."""
    _state.store.insert_plural(
        source_text=source_text, source_lang=source_lang, target_lang=target_lang,
        project_context_hash=project_context_hash,
        string_context_hash=string_context_hash,
        plural_category=plural_category,
        translated_text=translated_text, model=model, status=status,
        string_context=string_context,
    )
    if _state.shared_store is not None:
        try:
            _state.shared_store.insert_plural(
                source_text=source_text, source_lang=source_lang, target_lang=target_lang,
                project_context_hash=project_context_hash,
                string_context_hash=string_context_hash,
                plural_category=plural_category,
                translated_text=translated_text, model=model, status=status,
                string_context=string_context,
            )
        except Exception:
            logger.warning("Failed to write plural to shared store", exc_info=True)


def detect_language(text: str) -> str:
    """Detect the language of a text string. Returns an uppercase ISO 639-1 code."""
    if _state.config is None:
        raise RuntimeError("transduck not initialized. Call transduck.initialize() first.")

    from transduck import backend
    return backend.detect_language(text, _state.config)


def ait_plural(
    one: str,
    other: str,
    count: int | float,
    context: str | None = None,
    vars: dict | None = None,
    source_lang: str | None = None,
    background: bool = False,
    on_translate=None,
) -> TranslationResult:
    # Lazy import babel — only when pluralization is actually used
    from transduck.plural import get_plural_category, get_plural_categories

    if _state.config is None or _state.store is None:
        raise RuntimeError("transduck not initialized. Call transduck.initialize() first.")
    if _state.target_lang is None:
        raise RuntimeError("Target language not set. Call transduck.set_language() first.")

    cfg = _state.config
    src_lang = (source_lang or cfg.source_lang).upper()
    tgt_lang = _state.target_lang

    # Build vars with count
    if vars is None:
        vars = {"count": count}
    elif "count" not in vars:
        vars = {**vars, "count": count}

    # Same language, 2-form language: select directly from provided forms
    if tgt_lang == src_lang:
        categories = get_plural_categories(src_lang)
        if categories <= {"one", "other"}:
            category = get_plural_category(src_lang, count)
            form = one if category == "one" else other
            return _make_result(form, pending=False, source_lang=src_lang,
                                lang=tgt_lang, vars=vars)

    # Build cache key
    source_key = one + "\x00" + other
    project_context_hash = _hash(cfg.project_context)
    string_context_hash = _hash(context or "")

    # Tier 1: local SQLite lookup
    cached = _state.store.lookup_plural(
        source_text=source_key,
        source_lang=src_lang,
        target_lang=tgt_lang,
        project_context_hash=project_context_hash,
        string_context_hash=string_context_hash,
    )

    category = get_plural_category(tgt_lang, count)
    if category in cached:
        _maybe_fire_hooks(source_text=source_key, translated_text=cached[category],
                          source_lang=src_lang, lang=tgt_lang, context=context,
                          source="cache", on_translate=on_translate, plural=True,
                          plural_category=category, plural_forms=cached)
        return _make_result(cached[category], pending=False, source_lang=src_lang,
                            lang=tgt_lang, vars=vars, source="cache")

    # Tier 2: shared Postgres lookup (if configured)
    if _state.shared_store is not None:
        shared_cached = _state.shared_store.lookup_plural(
            source_text=source_key,
            source_lang=src_lang,
            target_lang=tgt_lang,
            project_context_hash=project_context_hash,
            string_context_hash=string_context_hash,
        )
        if category in shared_cached:
            # Propagate all forms to local store
            for cat, text in shared_cached.items():
                _state.store.insert_plural(
                    source_text=source_key, source_lang=src_lang, target_lang=tgt_lang,
                    project_context_hash=project_context_hash,
                    string_context_hash=string_context_hash,
                    plural_category=cat, translated_text=text,
                    model=cfg.backend_model, string_context=context or "",
                )
            _maybe_fire_hooks(source_text=source_key, translated_text=shared_cached[category],
                              source_lang=src_lang, lang=tgt_lang, context=context,
                              source="shared_cache", on_translate=on_translate, plural=True,
                              plural_category=category, plural_forms=shared_cached)
            return _make_result(shared_cached[category], pending=False,
                                source_lang=src_lang, lang=tgt_lang, vars=vars,
                                source="shared_cache")

    # Read-only mode: skip backend, fall back to source forms
    if cfg.read_only:
        logger.debug("Read-only mode, cache miss for plural: %s / %s", one, other)
        fallback_category = get_plural_category(src_lang, count)
        fallback = one if fallback_category == "one" else other
        return _make_result(fallback, pending=False, source_lang=src_lang,
                            lang=tgt_lang, vars=vars)

    # Background mode
    if background:
        content_hash = (source_key, src_lang, tgt_lang, project_context_hash, string_context_hash)
        with _state._inflight_lock:
            if content_hash in _state._inflight:
                fallback_category = get_plural_category(src_lang, count)
                fallback = one if fallback_category == "one" else other
                return _make_result(fallback, pending=True, source_lang=src_lang,
                                    lang=tgt_lang, vars=vars)
            _state._inflight.add(content_hash)
            _state._bg_done.clear()

        def _bg_translate_plural():
            try:
                forms = _do_translate_plural(one, other, source_key, src_lang, tgt_lang,
                                              project_context_hash, string_context_hash, context, cfg)
                if forms is not None and category in forms:
                    _maybe_fire_hooks(source_text=source_key, translated_text=forms[category],
                                      source_lang=src_lang, lang=tgt_lang, context=context,
                                      source="translated", on_translate=on_translate, plural=True,
                                      plural_category=category, plural_forms=forms)
            finally:
                with _state._inflight_lock:
                    _state._inflight.discard(content_hash)
                    if not _state._inflight:
                        _state._bg_done.set()

        threading.Thread(target=_bg_translate_plural, daemon=True).start()
        fallback_category = get_plural_category(src_lang, count)
        fallback = one if fallback_category == "one" else other
        return _make_result(fallback, pending=True, source_lang=src_lang,
                            lang=tgt_lang, vars=vars)

    # Synchronous plural translation
    forms = _do_translate_plural(one, other, source_key, src_lang, tgt_lang,
                                 project_context_hash, string_context_hash, context, cfg)
    if forms is not None:
        source_placeholders = extract_placeholders(one) | extract_placeholders(other)
        if category in forms:
            text = forms[category]
            tp = extract_placeholders(text)
            if source_placeholders.issubset(tp):
                _maybe_fire_hooks(source_text=source_key, translated_text=text,
                                  source_lang=src_lang, lang=tgt_lang, context=context,
                                  source="translated", on_translate=on_translate, plural=True,
                                  plural_category=category, plural_forms=forms)
                return _make_result(text, pending=False, source_lang=src_lang,
                                    lang=tgt_lang, vars=vars, source="translated")

    # Fallback
    fallback_category = get_plural_category(src_lang, count)
    fallback = one if fallback_category == "one" else other
    return _make_result(fallback, pending=False, source_lang=src_lang,
                        lang=tgt_lang, vars=vars)


def _do_translate_plural(one, other, source_key, src_lang, tgt_lang,
                         project_context_hash, string_context_hash, context, cfg):
    """Perform plural translation. Returns the forms dict or None."""
    from transduck import backend
    try:
        forms = backend.translate_plural(
            one=one,
            other=other,
            source_lang=src_lang,
            target_lang=tgt_lang,
            project_context=cfg.project_context,
            string_context=context,
            config=cfg,
        )
    except Exception:
        logger.warning("Backend failed for plural: %s / %s", one, other, exc_info=True)
        return None

    # Validate and store each form
    valid_categories = {"zero", "one", "two", "few", "many", "other"}
    source_placeholders = extract_placeholders(one) | extract_placeholders(other)

    if not isinstance(forms, dict) or "other" not in forms:
        logger.warning("Invalid plural response for: %s / %s", one, other)
        return None

    for cat, text in forms.items():
        if cat not in valid_categories or not text:
            continue
        translated_placeholders = extract_placeholders(text)
        status = "translated" if source_placeholders.issubset(translated_placeholders) else "failed"
        _insert_plural_both(
            source_text=source_key, source_lang=src_lang, target_lang=tgt_lang,
            project_context_hash=project_context_hash,
            string_context_hash=string_context_hash,
            plural_category=cat, translated_text=text,
            model=cfg.backend_model, status=status,
            string_context=context or "",
        )

    return forms
