import json
import pathlib
import hashlib
from collections import OrderedDict
from typing import Dict, Any, Optional, Iterable, List, Tuple
import logging
from jinja2 import Environment, meta, nodes, Template
from jinja2.visitor import NodeVisitor
from .synthia import סינתיה

logger = logging.getLogger(__name__)


class _מנתח_תבנית(NodeVisitor):
    def __init__(self) -> None:
        self.נתיבי_גישה: List[Tuple[Any, ...]] = []
        self._מחסנית_כינויים_לולאה: List[Dict[str, List[Any]]] = []
        self._כינויים_השמה: Dict[str, List[Any]] = {}

    def נתח(self, עץ: nodes.Node) -> List[Tuple[Any, ...]]:
        self.visit(עץ)
        return self.נתיבי_גישה

    def analyze(self, ast: nodes.Node) -> List[Tuple[Any, ...]]:
        return self.נתח(ast)

    # NodeVisitor hooks (must keep English names)
    def visit_For(self, node: nodes.For) -> None:
        self._בקר_לולאה(node)

    def visit_Assign(self, node: nodes.Assign) -> None:
        self._בקר_השמה(node)

    def visit_Getattr(self, node: nodes.Getattr) -> None:
        self._בקר_גישה(node)

    def visit_Getitem(self, node: nodes.Getitem) -> None:
        self._בקר_גישה(node)

    # ---------- לוגיקה בעברית ----------

    def _בקר_לולאה(self, צומת: nodes.For) -> None:
        שמות_כינוי = self._חלץ_מטרות_לולאה(צומת.target)
        נתיב_איטרציה = self._ביטוי_לנתיב(צומת.iter)
        if נתיב_איטרציה:
            מיפוי = {שם: נתיב_איטרציה + ["*"] for שם in שמות_כינוי}
            self._מחסנית_כינויים_לולאה.append(מיפוי)
        else:
            self._מחסנית_כינויים_לולאה.append({})
        self.generic_visit(צומת)
        self._מחסנית_כינויים_לולאה.pop()

    def _בקר_השמה(self, צומת: nodes.Assign) -> None:
        if isinstance(צומת.target, nodes.Name):
            נתיב_ערך = self._ביטוי_לנתיב(צומת.node)
            if נתיב_ערך:
                self._כינויים_השמה[צומת.target.name] = נתיב_ערך
        self.generic_visit(צומת)

    def _בקר_גישה(self, צומת: nodes.Node) -> None:
        נתיב = self._ביטוי_לנתיב(צומת)
        נתיב_פתור = self._פתור_נתיב(נתיב)
        if נתיב_פתור and len(נתיב_פתור) > 1:
            self.נתיבי_גישה.append(tuple(נתיב_פתור))
        self.generic_visit(צומת)

    def _חלץ_מטרות_לולאה(self, יעד: nodes.Node) -> List[str]:
        if isinstance(יעד, nodes.Name):
            return [יעד.name]
        if isinstance(יעד, nodes.Tuple):
            שמות = []
            for פריט in יעד.items:
                if isinstance(פריט, nodes.Name):
                    שמות.append(פריט.name)
            return שמות
        return []

    def _פרק_ביטוי(self, צומת: nodes.Node) -> nodes.Node:
        while True:
            if isinstance(צומת, nodes.Filter):
                צומת = צומת.node
                continue
            if isinstance(צומת, nodes.Test):
                צומת = צומת.node
                continue
            if isinstance(צומת, nodes.Call):
                צומת = צומת.node
                continue
            if isinstance(צומת, nodes.UnaryExpr):
                צומת = צומת.node
                continue
            return צומת

    def _ביטוי_לנתיב(self, צומת: Optional[nodes.Node]) -> Optional[List[Any]]:
        if צומת is None:
            return None
        צומת = self._פרק_ביטוי(צומת)
        if isinstance(צומת, nodes.Name):
            return [צומת.name]
        if isinstance(צומת, nodes.Getattr):
            בסיס = self._ביטוי_לנתיב(צומת.node)
            if not בסיס:
                return None
            return בסיס + [צומת.attr]
        if isinstance(צומת, nodes.Getitem):
            בסיס = self._ביטוי_לנתיב(צומת.node)
            if not בסיס:
                return None
            מפתח = self._קבל_מפתח_קבוע(צומת.arg)
            if מפתח is None:
                מפתח = "*"
            return בסיס + [מפתח]
        return None

    def _קבל_מפתח_קבוע(self, צומת: nodes.Node) -> Optional[Any]:
        if isinstance(צומת, nodes.Const):
            if isinstance(צומת.value, int):
                return "*"
            return צומת.value
        if isinstance(צומת, nodes.Slice):
            return "*"
        return None

    def _פתור_נתיב(self, נתיב: Optional[List[Any]]) -> Optional[List[Any]]:
        if not נתיב:
            return None
        בסיס = נתיב[0]
        נתיב_כינוי = self._פתור_כינוי(בסיס)
        if נתיב_כינוי:
            return נתיב_כינוי + נתיב[1:]
        return נתיב

    def _פתור_כינוי(self, שם: str) -> Optional[List[Any]]:
        for תחום in reversed(self._מחסנית_כינויים_לולאה):
            if שם in תחום:
                return תחום[שם]
        return self._כינויים_השמה.get(שם)


class פלמינגו:
    """
    פלמינגו
    ---------
    רנדרר צד-שרת בסיוע LLM עם Jinja2.
    """

    def __init__(self, מפתח_אבטחה: str, תיקיית_תבניות: str = "templates", **kwargs):
        if "security_key" in kwargs:
            מפתח_אבטחה = kwargs["security_key"]
        if "templates_dir" in kwargs:
            תיקיית_תבניות = kwargs["templates_dir"]

        self.מודל = סינתיה(מפתח_אבטחה)
        self.llm = self.מודל  # תאימות אנגלית
        self.תיקיית_תבניות = pathlib.Path(תיקיית_תבניות)
        self.templates_dir = self.תיקיית_תבניות  # תאימות אנגלית
        self._סביבת_גיניה = Environment()
        self._jinja_env = self._סביבת_גיניה  # תאימות אנגלית
        self._מטמון_תבניות: Dict[str, Dict[str, Any]] = {}
        self._template_cache = self._מטמון_תבניות
        self._מטמון_מובנה: "OrderedDict[Tuple[Any, ...], Dict[str, Any]]" = OrderedDict()
        self._structured_cache = self._מטמון_מובנה
        self._מגבלת_מטמון_מובנה = 256
        self._structured_cache_limit = self._מגבלת_מטמון_מובנה

    # ---------------------------------------------------------
    # API ציבורי
    # ---------------------------------------------------------

    def רנדר(
        self,
        שם_תבנית: str,
        הקשר: Dict[str, Any],
        כוונה: str,
        מצב_מודל: str = "auto",
    ) -> str:
        תוכן_תבנית, זמן_שינוי = self._טען_תבנית(שם_תבנית)

        if not הקשר or all(v is None for v in הקשר.values()):
            return self._רנדר_גיניה(שם_תבנית, תוכן_תבנית, {})

        הקשר_גולמי = dict(הקשר)
        עקיפת_llm = הקשר_גולמי.pop("__flamingo_llm__", None)
        if עקיפת_llm is not None:
            מצב_מודל = "force" if עקיפת_llm else "never"

        הקשר_מנורמל = self._נרמל_הקשר(הקשר_גולמי)
        if מצב_מודל != "force":
            if מצב_מודל == "never" or not self._צריך_מודל(שם_תבנית, תוכן_תבנית, הקשר_מנורמל):
                return self._רנדר_גיניה(שם_תבנית, תוכן_תבנית, הקשר_מנורמל)

        מפתח_מטמון = self._מפתח_מטמון_מובנה(שם_תבנית, זמן_שינוי, כוונה, הקשר_גולמי)
        הקשר_מובנה = self._מטמון_מובנה.get(מפתח_מטמון)
        if הקשר_מובנה is None:
            הקשר_מובנה = self._חלץ_הקשר(
                תבנית=תוכן_תבנית,
                הקשר_גולמי=הקשר_גולמי,
                כוונה=כוונה
            )
            self._מטמון_מובנה[מפתח_מטמון] = הקשר_מובנה
            self._מטמון_מובנה.move_to_end(מפתח_מטמון)
            if len(self._מטמון_מובנה) > self._מגבלת_מטמון_מובנה:
                self._מטמון_מובנה.popitem(last=False)

        return self._רנדר_גיניה(שם_תבנית, תוכן_תבנית, הקשר_מובנה)

    def render(
        self,
        template_name: str,
        context: Dict[str, Any],
        intent: str,
        llm_mode: str = "auto",
    ) -> str:
        return self.רנדר(template_name, context, intent, llm_mode)

    # ---------------------------------------------------------
    # עזר פנימי
    # ---------------------------------------------------------

    def _טען_תבנית(self, שם_תבנית: str) -> Tuple[str, float]:
        נתיב = self.תיקיית_תבניות / שם_תבנית
        if not נתיב.exists():
            raise FileNotFoundError(f"Template not found: {שם_תבנית}")
        זמן_שינוי = נתיב.stat().st_mtime
        מטמון = self._מטמון_תבניות.get(שם_תבנית)
        if מטמון and מטמון["mtime"] == זמן_שינוי:
            return מטמון["content"], מטמון["mtime"]
        תוכן = נתיב.read_text(encoding="utf-8")
        מקומפל = self._סביבת_גיניה.from_string(תוכן)
        self._מטמון_תבניות[שם_תבנית] = {
            "content": תוכן,
            "mtime": זמן_שינוי,
            "compiled": מקומפל,
            "access_paths": None,
        }
        return תוכן, זמן_שינוי

    def _חלץ_הקשר(self, תבנית: str, הקשר_גולמי: Dict, כוונה: str) -> Dict[str, Any]:
        הקשר_כמחרוזת = self._המר_הקשר_למחרוזת(הקשר_גולמי)
        משתנים = self._חלץ_משתני_תבנית(תבנית)

        הנחיה = f"""
You are Flamingo, a context extractor for Jinja2 templates.

TASK: Analyze the Jinja2 template variables and extract/format a JSON context object that matches what the template expects.

RULES:
1. Look at the template's Jinja2 variables: {משתנים}
2. Extract corresponding data from the raw context
3. Format data to match template structure exactly
4. Return ONLY valid JSON - no explanations, no markdown
5. If raw context has dict/list data, preserve the structure
6. If raw context has status info, include it

--------------------
INTENT: {כוונה}

--------------------
RAW CONTEXT:
{הקשר_כמחרוזת}

--------------------
Return ONLY the JSON context object:
"""

        try:
            תגובה = self.מודל.השלם(הנחיה)

            תגובה = תגובה.strip()
            if תגובה.startswith("```json"):
                תגובה = תגובה[7:]
            if תגובה.startswith("```"):
                תגובה = תגובה[3:]
            if תגובה.endswith("```"):
                תגובה = תגובה[:-3]
            תגובה = תגובה.strip()

            try:
                הקשר_מובנה = json.loads(תגובה)
                return הקשר_מובנה
            except json.JSONDecodeError:
                logger.exception("[Flamingo] JSON parse error")
                logger.debug("[Flamingo] Response was: %s", תגובה)
                return הקשר_גולמי
        except Exception:
            logger.exception("[Flamingo] LLM call failed")
            logger.warning("[Flamingo] Falling back to raw context")
            return הקשר_גולמי
            תגובה = תגובה[3:]

    def _חלץ_משתני_תבנית(self, תבנית: str) -> str:
        try:
            עץ = self._סביבת_גיניה.parse(תבנית)
            משתנים = meta.find_undeclared_variables(עץ)
            return ", ".join(sorted(משתנים))
        except Exception:
            logger.exception("[Flamingo] Error extracting variables")
            return "Unable to extract variables"

    def _המר_הקשר_למחרוזת(self, הקשר: Dict[str, Any]) -> str:
        שורות = []
        for מפתח, ערך in הקשר.items():
            if ערך is None:
                שורות.append(f"{מפתח}: None")
            elif isinstance(ערך, (str, int, float, bool)):
                שורות.append(f"{מפתח}: {ערך}")
            elif isinstance(ערך, (list, dict)):
                try:
                    שורות.append(f"{מפתח}: {json.dumps(ערך, indent=2, sort_keys=True, default=str)}")
                except TypeError:
                    שורות.append(f"{מפתח}: {repr(ערך)}")
            else:
                שורות.append(f"{מפתח}: {repr(ערך)}")
        return "\n".join(שורות)

    def _רנדר_גיניה(self, שם_תבנית: str, תוכן_תבנית: str, הקשר: Dict[str, Any]) -> str:
        def _ודא_דוקטייפ(מרונדר: str) -> str:
            if not מרונדר.strip().lower().startswith("<!doctype"):
                return "<!DOCTYPE html>\n" + מרונדר
            return מרונדר

        תבנית = None
        try:
            מטמון = self._מטמון_תבניות.get(שם_תבנית)
            if מטמון and מטמון.get("content") == תוכן_תבנית:
                תבנית = מטמון.get("compiled")
            else:
                תבנית = self._סביבת_גיניה.from_string(תוכן_תבנית)
            if תבנית is None:
                תבנית = self._סביבת_גיניה.from_string(תוכן_תבנית)
            מרונדר = תבנית.render(**הקשר)
            return _ודא_דוקטייפ(מרונדר)
        except Exception:
            logger.exception("[Flamingo] Jinja2 render error")
            try:
                מרונדר = תבנית.render(הקשר)
                return _ודא_דוקטייפ(מרונדר)
            except Exception:
                logger.exception("[Flamingo] Jinja2 render retry failed")
                try:
                    תבנית_חלופית = Template(תוכן_תבנית)
                    מרונדר = תבנית_חלופית.render(**הקשר)
                    return _ודא_דוקטייפ(מרונדר)
                except Exception:
                    logger.exception("[Flamingo] Jinja2 fallback render failed")
                    return תוכן_תבנית

    def _צריך_מודל(self, שם_תבנית: str, תוכן_תבנית: str, הקשר: Dict[str, Any]) -> bool:
        נתיבי_גישה = self._קבל_נתיבי_גישה(שם_תבנית, תוכן_תבנית)
        if not נתיבי_גישה:
            return False
        for נתיב in נתיבי_גישה:
            if self._נתיב_אי_התאמת_טיפוס(נתיב, הקשר):
                return True
        return False

    def _קבל_נתיבי_גישה(self, שם_תבנית: str, תוכן_תבנית: str) -> List[Tuple[Any, ...]]:
        מטמון = self._מטמון_תבניות.get(שם_תבנית)
        if מטמון and מטמון.get("access_paths") is not None and מטמון.get("content") == תוכן_תבנית:
            return מטמון["access_paths"]
        try:
            עץ = self._סביבת_גיניה.parse(תוכן_תבנית)
            מנתח = _מנתח_תבנית()
            נתיבי_גישה = מנתח.נתח(עץ)
        except Exception:
            logger.exception("[Flamingo] Template analysis failed")
            נתיבי_גישה = []
        if מטמון and מטמון.get("content") == תוכן_תבנית:
            מטמון["access_paths"] = נתיבי_גישה
        return נתיבי_גישה

    def _נתיב_אי_התאמת_טיפוס(self, נתיב: Tuple[Any, ...], הקשר: Dict[str, Any]) -> bool:
        ערך: Any = הקשר
        for קטע in נתיב:
            if קטע == "*":
                if not isinstance(ערך, (list, tuple)):
                    return True
                if not ערך:
                    return False
                ערך = ערך[0]
                continue
            if isinstance(ערך, dict):
                if קטע not in ערך:
                    return False
                ערך = ערך[קטע]
                continue
            if hasattr(ערך, קטע):
                ערך = getattr(ערך, קטע)
                continue
            return True
        return False

    def _נרמל_הקשר(self, הקשר: Dict[str, Any]) -> Dict[str, Any]:
        מנורמל = {}
        for מפתח, ערך in הקשר.items():
            מנורמל[str(מפתח)] = self._נרמל_ערך(ערך)
        return מנורמל

    def _נרמל_ערך(self, ערך: Any) -> Any:
        if isinstance(ערך, str):
            מפורש = self._נסה_לפרש_json(ערך)
            return מפורש if מפורש is not None else ערך
        if isinstance(ערך, dict):
            return {str(k): self._נרמל_ערך(v) for k, v in ערך.items()}
        if isinstance(ערך, list):
            return [self._נרמל_ערך(v) for v in ערך]
        if isinstance(ערך, tuple):
            return tuple(self._נרמל_ערך(v) for v in ערך)
        return ערך

    def _נסה_לפרש_json(self, ערך: str) -> Optional[Any]:
        טקסט = ערך.strip()
        if טקסט.startswith("```"):
            טקסט = self._הסר_גדרות_קוד(טקסט)
        if not טקסט or טקסט[0] not in "{[":
            return None
        if len(טקסט) > 20000:
            return None
        try:
            return json.loads(טקסט)
        except json.JSONDecodeError:
            return None

    def _הסר_גדרות_קוד(self, טקסט: str) -> str:
        מופשט = טקסט.strip()
        if מופשט.startswith("```json"):
            מופשט = מופשט[7:]
        elif מופשט.startswith("```"):
            מופשט = מופשט[3:]
        if מופשט.endswith("```"):
            מופשט = מופשט[:-3]
        return מופשט.strip()

    def _מפתח_מטמון_מובנה(self, שם_תבנית: str, זמן_שינוי: float, כוונה: str, הקשר: Dict[str, Any]) -> Tuple[Any, ...]:
        הקשר_כמחרוזת = self._המר_הקשר_למחרוזת(הקשר)
        גיבוב = hashlib.sha256(הקשר_כמחרוזת.encode("utf-8")).hexdigest()
        return (שם_תבנית, זמן_שינוי, כוונה, גיבוב)

    def רנדר_ישיר(self, שם_תבנית: str, הקשר: Dict[str, Any], כוונה: str | None = None) -> str:
        תוכן, _ = self._טען_תבנית(שם_תבנית)
        return self._רנדר_גיניה(שם_תבנית, תוכן, הקשר)

    # --- תאימות אנגלית ---
    render_direct = רנדר_ישיר


# תאימות שמות
Flamingo = פלמינגו
