"""Cypher query builder — programmatic construction of Cypher statements.

Covers the 80% case: MATCH/CREATE/MERGE/WHERE/RETURN/SET/DELETE/WITH/ORDER BY/LIMIT/SKIP.
Each builder produces ``(cypher_string, params_tuple)`` for ``driver.cypher()``.
"""

from __future__ import annotations

from typing import Any

from piggie._util import validate_label
from piggie.exceptions import BuilderError

# ---------------------------------------------------------------------------
# Operator mapping for Django-style WHERE filters
# ---------------------------------------------------------------------------

_OPS: dict[str, str] = {
    "gt": ">",
    "gte": ">=",
    "lt": "<",
    "lte": "<=",
    "ne": "<>",
    "contains": "CONTAINS",
    "starts_with": "STARTS WITH",
    "ends_with": "ENDS WITH",
    "in": "IN",
    "is_null": "IS NULL",
    "is_not_null": "IS NOT NULL",
}


def _quote(value: Any) -> tuple[str, tuple[Any, ...]]:
    """Return a (%s placeholder, param) pair for a value."""
    return "%s", (value,)


def _props_literal(props: dict[str, Any]) -> tuple[str, tuple[Any, ...]]:
    """Build ``{key1: %s, key2: %s}`` and collect params."""
    if not props:
        return "", ()
    parts: list[str] = []
    params: list[Any] = []
    for k, v in props.items():
        parts.append(f"{k}: %s")
        params.append(v)
    return "{" + ", ".join(parts) + "}", tuple(params)


# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------


class CypherBuilder:
    """Abstract base for Cypher builders."""

    def build(self) -> tuple[str, tuple[Any, ...]]:
        raise NotImplementedError


# ---------------------------------------------------------------------------
# Match
# ---------------------------------------------------------------------------


class Match(CypherBuilder):
    """Build a MATCH … RETURN query.

    Example::

        q, params = (
            Match("Person", alias="p", name="Alice")
            .rel("KNOWS", alias="r")
            .node("Person", alias="q")
            .where(p__age__gt=30)
            .return_("p.name", "q.name")
            .order_by("p.name")
            .limit(10)
            .build()
        )
        result = driver.cypher(q, params=params)
    """

    def __init__(
        self,
        label: str | None = None,
        alias: str | None = None,
        **properties: Any,
    ) -> None:
        self._pattern_parts: list[dict[str, Any]] = []
        self._where_clauses: list[str] = []
        self._where_params: list[Any] = []
        self._return_exprs: list[str] = []
        self._with_exprs: list[str] = []
        self._order_exprs: list[str] = []
        self._order_desc = False
        self._limit_val: int | None = None
        self._skip_val: int | None = None
        self._set_alias: str | None = None
        self._set_props: dict[str, Any] = {}
        self._delete_aliases: list[str] = []
        self._detach_delete = False

        if label is not None:
            validate_label(label)
        self._pattern_parts.append({
            "type": "node",
            "label": label,
            "alias": alias,
            "props": properties,
        })

    def rel(
        self,
        rel_type: str | None = None,
        alias: str | None = None,
        direction: str = "out",
        **properties: Any,
    ) -> Match:
        if rel_type is not None:
            validate_label(rel_type)
        self._pattern_parts.append({
            "type": "rel",
            "rel_type": rel_type,
            "alias": alias,
            "direction": direction,
            "props": properties,
        })
        return self

    def node(
        self,
        label: str | None = None,
        alias: str | None = None,
        **properties: Any,
    ) -> Match:
        if label is not None:
            validate_label(label)
        self._pattern_parts.append({
            "type": "node",
            "label": label,
            "alias": alias,
            "props": properties,
        })
        return self

    def where(self, raw: str | None = None, **kwargs: Any) -> Match:
        """Add WHERE conditions.

        Supports raw Cypher strings and Django-style keyword filters::

            .where(p__age__gt=30)        # p.age > %s
            .where(p__name="Alice")      # p.name = %s
            .where(raw="p.age > 30")     # literal
        """
        if raw:
            self._where_clauses.append(raw)
        for key, val in kwargs.items():
            parts = key.split("__")
            if len(parts) == 3:
                alias, prop, op = parts
                if op not in _OPS:
                    raise BuilderError(f"Unknown operator: {op}")
                cypher_op = _OPS[op]
                if op in ("is_null", "is_not_null"):
                    self._where_clauses.append(f"{alias}.{prop} {cypher_op}")
                elif op == "in":
                    self._where_clauses.append(f"{alias}.{prop} IN %s")
                    self._where_params.append(val)
                else:
                    self._where_clauses.append(f"{alias}.{prop} {cypher_op} %s")
                    self._where_params.append(val)
            elif len(parts) == 2:
                alias, prop = parts
                self._where_clauses.append(f"{alias}.{prop} = %s")
                self._where_params.append(val)
            else:
                raise BuilderError(
                    f"Invalid where key: {key!r}. Use alias__prop or alias__prop__op."
                )
        return self

    def return_(self, *exprs: str) -> Match:
        self._return_exprs = list(exprs)
        return self

    def return_all(self) -> Match:
        self._return_exprs = ["*"]
        return self

    def order_by(self, *exprs: str, desc: bool = False) -> Match:
        self._order_exprs = list(exprs)
        self._order_desc = desc
        return self

    def limit(self, n: int) -> Match:
        self._limit_val = n
        return self

    def skip(self, n: int) -> Match:
        self._skip_val = n
        return self

    def with_(self, *exprs: str) -> Match:
        self._with_exprs = list(exprs)
        return self

    def set(self, alias: str, **properties: Any) -> Match:
        self._set_alias = alias
        self._set_props = properties
        return self

    def delete(self, *aliases: str, detach: bool = False) -> Match:
        self._delete_aliases = list(aliases)
        self._detach_delete = detach
        return self

    def build(self) -> tuple[str, tuple[Any, ...]]:
        parts: list[str] = []
        params: list[Any] = []

        # MATCH pattern
        pattern = self._build_pattern(params)
        parts.append(f"MATCH {pattern}")

        # WHERE
        if self._where_clauses:
            parts.append("WHERE " + " AND ".join(self._where_clauses))
            params.extend(self._where_params)

        # WITH
        if self._with_exprs:
            parts.append("WITH " + ", ".join(self._with_exprs))

        # SET
        if self._set_alias and self._set_props:
            set_parts = []
            for k, v in self._set_props.items():
                set_parts.append(f"{self._set_alias}.{k} = %s")
                params.append(v)
            parts.append("SET " + ", ".join(set_parts))

        # DELETE
        if self._delete_aliases:
            kw = "DETACH DELETE" if self._detach_delete else "DELETE"
            parts.append(f"{kw} " + ", ".join(self._delete_aliases))

        # RETURN
        if self._return_exprs:
            parts.append("RETURN " + ", ".join(self._return_exprs))

        # ORDER BY
        if self._order_exprs:
            order = "ORDER BY " + ", ".join(self._order_exprs)
            if self._order_desc:
                order += " DESC"
            parts.append(order)

        # SKIP
        if self._skip_val is not None:
            parts.append(f"SKIP {self._skip_val}")

        # LIMIT
        if self._limit_val is not None:
            parts.append(f"LIMIT {self._limit_val}")

        return " ".join(parts), tuple(params)

    def _build_pattern(self, params: list[Any]) -> str:
        """Build the MATCH pattern string like (p:Person)-[r:KNOWS]->(q:Person)."""
        buf: list[str] = []
        for part in self._pattern_parts:
            if part["type"] == "node":
                buf.append(self._node_str(part, params))
            elif part["type"] == "rel":
                buf.append(self._rel_str(part, params))
        return "".join(buf)

    @staticmethod
    def _node_str(part: dict[str, Any], params: list[Any]) -> str:
        alias = part.get("alias") or ""
        label = part.get("label")
        props = part.get("props", {})
        label_str = f":{label}" if label else ""
        props_str, p = _props_literal(props)
        params.extend(p)
        inner = f"{alias}{label_str}"
        if props_str:
            inner += f" {props_str}"
        return f"({inner})"

    @staticmethod
    def _rel_str(part: dict[str, Any], params: list[Any]) -> str:
        alias = part.get("alias") or ""
        rel_type = part.get("rel_type")
        direction = part.get("direction", "out")
        props = part.get("props", {})
        type_str = f":{rel_type}" if rel_type else ""
        props_str, p = _props_literal(props)
        params.extend(p)
        inner = f"{alias}{type_str}"
        if props_str:
            inner += f" {props_str}"
        if direction == "in":
            return f"<-[{inner}]-"
        elif direction == "both":
            return f"-[{inner}]-"
        else:
            return f"-[{inner}]->"

    def __repr__(self) -> str:
        cypher, _ = self.build()
        return f"Match({cypher!r})"


# ---------------------------------------------------------------------------
# Create
# ---------------------------------------------------------------------------


class Create(CypherBuilder):
    """Build a CREATE query.

    Example::

        q, params = (
            Create("Person", name="Alice", age=30)
            .return_("n")
            .build()
        )
    """

    def __init__(self, label: str, alias: str = "n", **properties: Any) -> None:
        validate_label(label)
        self._label = label
        self._alias = alias
        self._properties = properties
        self._rel_chain: list[dict[str, Any]] = []
        self._return_exprs: list[str] = []

    def rel(
        self,
        rel_type: str,
        target_label: str,
        direction: str = "out",
        target_alias: str | None = None,
        **rel_properties: Any,
    ) -> Create:
        validate_label(rel_type)
        validate_label(target_label)
        self._rel_chain.append({
            "rel_type": rel_type,
            "target_label": target_label,
            "direction": direction,
            "target_alias": target_alias,
            "rel_props": rel_properties,
        })
        return self

    def return_(self, *exprs: str) -> Create:
        self._return_exprs = list(exprs)
        return self

    def build(self) -> tuple[str, tuple[Any, ...]]:
        params: list[Any] = []
        props_str, p = _props_literal(self._properties)
        params.extend(p)

        inner = f"{self._alias}:{self._label}"
        if props_str:
            inner += f" {props_str}"
        pattern = f"({inner})"

        # Relationship chain
        for i, link in enumerate(self._rel_chain):
            rel_props_str, rp = _props_literal(link["rel_props"])
            params.extend(rp)
            rel_inner = f":{link['rel_type']}"
            if rel_props_str:
                rel_inner += f" {rel_props_str}"

            t_alias = link.get("target_alias") or f"m{i}"
            target = f"({t_alias}:{link['target_label']})"

            if link["direction"] == "in":
                pattern += f"<-[{rel_inner}]-{target}"
            elif link["direction"] == "both":
                pattern += f"-[{rel_inner}]-{target}"
            else:
                pattern += f"-[{rel_inner}]->{target}"

        parts = [f"CREATE {pattern}"]
        if self._return_exprs:
            parts.append("RETURN " + ", ".join(self._return_exprs))

        return " ".join(parts), tuple(params)

    def __repr__(self) -> str:
        cypher, _ = self.build()
        return f"Create({cypher!r})"


# ---------------------------------------------------------------------------
# Merge
# ---------------------------------------------------------------------------


class Merge(CypherBuilder):
    """Build a MERGE query with optional ON CREATE SET / ON MATCH SET.

    Example::

        q, params = (
            Merge("Person", name="Alice")
            .on_create(created_at="2024-01-01")
            .on_match(last_seen="2024-06-01")
            .set(active=True)
            .return_("n")
            .build()
        )
    """

    def __init__(self, label: str, alias: str = "n", **match_properties: Any) -> None:
        validate_label(label)
        self._label = label
        self._alias = alias
        self._match_props = match_properties
        self._on_create_props: dict[str, Any] = {}
        self._on_match_props: dict[str, Any] = {}
        self._set_props: dict[str, Any] = {}
        self._return_exprs: list[str] = []

    def on_create(self, **properties: Any) -> Merge:
        self._on_create_props.update(properties)
        return self

    def on_match(self, **properties: Any) -> Merge:
        self._on_match_props.update(properties)
        return self

    def set(self, **properties: Any) -> Merge:
        self._set_props.update(properties)
        return self

    def return_(self, *exprs: str) -> Merge:
        self._return_exprs = list(exprs)
        return self

    def build(self) -> tuple[str, tuple[Any, ...]]:
        params: list[Any] = []
        props_str, p = _props_literal(self._match_props)
        params.extend(p)

        inner = f"{self._alias}:{self._label}"
        if props_str:
            inner += f" {props_str}"

        parts = [f"MERGE ({inner})"]

        # ON CREATE SET
        if self._on_create_props:
            set_parts = []
            for k, v in self._on_create_props.items():
                set_parts.append(f"{self._alias}.{k} = %s")
                params.append(v)
            parts.append("ON CREATE SET " + ", ".join(set_parts))

        # ON MATCH SET
        if self._on_match_props:
            set_parts = []
            for k, v in self._on_match_props.items():
                set_parts.append(f"{self._alias}.{k} = %s")
                params.append(v)
            parts.append("ON MATCH SET " + ", ".join(set_parts))

        # SET
        if self._set_props:
            set_parts = []
            for k, v in self._set_props.items():
                set_parts.append(f"{self._alias}.{k} = %s")
                params.append(v)
            parts.append("SET " + ", ".join(set_parts))

        # RETURN
        if self._return_exprs:
            parts.append("RETURN " + ", ".join(self._return_exprs))

        return " ".join(parts), tuple(params)

    def __repr__(self) -> str:
        cypher, _ = self.build()
        return f"Merge({cypher!r})"
