from __future__ import annotations

import re
import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

_SQL_DEFAULTS_PATH = Path(__file__).parent.parent.parent / "defaults" / "sql.json"
_AWS_DEFAULTS_PATH = Path(__file__).parent.parent.parent / "defaults" / "aws.json"  # for RDS config fallback


def load_sql_defaults() -> dict:
    """Read and return the full sql.json defaults dict (no caching)."""
    try:
        return json.loads(_SQL_DEFAULTS_PATH.read_text(encoding="utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        return {}


def get_engines(defaults: Optional[dict] = None) -> Dict[str, dict]:
    """Return the engines dict from defaults."""
    d = defaults if defaults is not None else load_sql_defaults()
    return d.get("engines", {})


def get_dsn_engine_map(defaults: Optional[dict] = None) -> Dict[str, str]:
    """Return the DSN-to-engine mapping with fallbacks."""
    d = defaults if defaults is not None else load_sql_defaults()
    m = dict(d.get("dsn_engine_map", {}))
    m.setdefault("postgresql", "postgres")
    m.setdefault("postgres", "postgres")
    m.setdefault("mysql", "mysql")
    m.setdefault("mariadb", "mariadb")
    return m


def get_connection_defaults(defaults: Optional[dict] = None) -> Dict[str, Any]:
    """Return the connection defaults with fallbacks."""
    d = defaults if defaults is not None else load_sql_defaults()
    c = dict(d.get("_config", {}).get("connection", d.get("connection", {})))
    c.setdefault("default_database", "mock")
    c.setdefault("default_host", "localhost")
    c.setdefault("autocommit", False)
    return c


def get_operation_defaults(operation: str, defaults: Optional[dict] = None) -> Dict[str, Any]:
    """Return defaults for a given SQL operation verb."""
    d = defaults if defaults is not None else load_sql_defaults()
    return dict(d.get("operations", {}).get(operation, {}))


def _load_aws_defaults_raw() -> dict:
    """Read and return the full aws.json defaults dict (no caching)."""
    try:
        return json.loads(_AWS_DEFAULTS_PATH.read_text(encoding="utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        return {}


def get_rds_defaults(defaults: Optional[dict] = None) -> Dict[str, Any]:
    """Return RDS-specific defaults (ARN templates, endpoints, region, account).

    Reads from ``_config.rds`` in the aws defaults dict. Falls back to loading
    aws.json directly if no defaults are provided.
    """
    if defaults is not None:
        rds = dict(defaults.get("_config", {}).get("rds", {}))
        if rds:
            rds.setdefault("default_region",
                           defaults.get("_config", {}).get("region", "us-east-1"))
            rds.setdefault("default_account",
                           defaults.get("_config", {}).get("account_id", "123456789012"))
    else:
        aws = _load_aws_defaults_raw()
        rds = dict(aws.get("_config", {}).get("rds", {}))
    if not rds:
        rds = {}
    rds.setdefault("default_region", "us-east-1")
    rds.setdefault("default_account", "123456789012")
    rds.setdefault("hosted_zone_id", "Z2R2ITUGPM61AM")
    rds.setdefault("instance_arn_template",
                    "arn:aws:rds:{region}:{account}:db:{identifier}")
    rds.setdefault("cluster_arn_template",
                    "arn:aws:rds:{region}:{account}:cluster:{identifier}")
    rds.setdefault("snapshot_arn_template",
                    "arn:aws:rds:{region}:{account}:snapshot:{identifier}")
    rds.setdefault("param_group_arn_template",
                    "arn:aws:rds:{region}:{account}:pg:{identifier}")
    rds.setdefault("cluster_param_group_arn_template",
                    "arn:aws:rds:{region}:{account}:cluster-pg:{identifier}")
    rds.setdefault("subnet_group_arn_template",
                    "arn:aws:rds:{region}:{account}:subgrp:{identifier}")
    rds.setdefault("option_group_arn_template",
                    "arn:aws:rds:{region}:{account}:og:{identifier}")
    rds.setdefault("event_subscription_arn_template",
                    "arn:aws:rds:{region}:{account}:es:{identifier}")
    rds.setdefault("proxy_arn_template",
                    "arn:aws:rds:{region}:{account}:db-proxy:{identifier}")
    rds.setdefault("endpoint_template",
                    "{identifier}.mock.{region}.rds.amazonaws.com")
    rds.setdefault("cluster_endpoint_template",
                    "{identifier}.cluster-mock.{region}.rds.amazonaws.com")
    rds.setdefault("cluster_reader_endpoint_template",
                    "{identifier}.cluster-ro-mock.{region}.rds.amazonaws.com")
    rds.setdefault("proxy_endpoint_template",
                    "{identifier}.proxy-mock.{region}.rds.amazonaws.com")
    return rds


def merge_overrides(base: dict, overrides: dict) -> dict:
    """Deep-merge user overrides into a copy of base defaults."""
    merged = dict(base)
    for key, val in overrides.items():
        if key == "_meta":
            continue
        if isinstance(val, dict) and isinstance(merged.get(key), dict):
            merged[key] = {**merged[key], **val}
        else:
            merged[key] = val
    return merged


# ---------------------------------------------------------------------------
# Engine / DSN helpers
# ---------------------------------------------------------------------------

def default_port(engine: str, defaults: Optional[dict] = None) -> int:
    engines = get_engines(defaults)
    cfg = engines.get(engine.lower(), {})
    return cfg.get("port", 5432)


def default_version(engine: str, defaults: Optional[dict] = None) -> str:
    engines = get_engines(defaults)
    cfg = engines.get(engine.lower(), {})
    return cfg.get("version", "8.0.35")


# ---------------------------------------------------------------------------
# RDS helpers — all driven by aws.json _config.rds templates
# ---------------------------------------------------------------------------

def _rds_format(template: str, identifier: str,
                defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return template.format(
        identifier=identifier,
        region=rds["default_region"],
        account=rds["default_account"],
    )


def rds_endpoint(identifier: str, engine: str,
                 port: Optional[int] = None,
                 defaults: Optional[dict] = None) -> dict:
    rds = get_rds_defaults(defaults)
    p = port or default_port(engine, defaults)
    address = rds["endpoint_template"].format(
        identifier=identifier,
        region=rds["default_region"],
    )
    return {
        "Address":      address,
        "Port":         p,
        "HostedZoneId": rds["hosted_zone_id"],
    }


def rds_cluster_endpoint(identifier: str,
                         defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return rds["cluster_endpoint_template"].format(
        identifier=identifier,
        region=rds["default_region"],
    )


def rds_cluster_reader_endpoint(identifier: str,
                                defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return rds["cluster_reader_endpoint_template"].format(
        identifier=identifier,
        region=rds["default_region"],
    )


def rds_proxy_endpoint(identifier: str,
                       defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return rds["proxy_endpoint_template"].format(
        identifier=identifier,
        region=rds["default_region"],
    )


def rds_instance_arn(identifier: str,
                     defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["instance_arn_template"], identifier, defaults)


def rds_cluster_arn(identifier: str,
                    defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["cluster_arn_template"], identifier, defaults)


def rds_snapshot_arn(identifier: str,
                     defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["snapshot_arn_template"], identifier, defaults)


def rds_param_group_arn(identifier: str,
                        defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["param_group_arn_template"], identifier, defaults)


def rds_cluster_param_group_arn(identifier: str,
                                defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["cluster_param_group_arn_template"],
                       identifier, defaults)


def rds_subnet_group_arn(identifier: str,
                         defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["subnet_group_arn_template"], identifier, defaults)


def rds_option_group_arn(identifier: str,
                         defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["option_group_arn_template"], identifier, defaults)


def rds_event_subscription_arn(identifier: str,
                               defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["event_subscription_arn_template"],
                       identifier, defaults)


def rds_proxy_arn(identifier: str,
                  defaults: Optional[dict] = None) -> str:
    rds = get_rds_defaults(defaults)
    return _rds_format(rds["proxy_arn_template"], identifier, defaults)


# ---------------------------------------------------------------------------
# MockQueryResult / MockSQLExecutor
# ---------------------------------------------------------------------------

class MockQueryResult:

    def __init__(
        self,
        rows:        List[Tuple],
        description: Optional[List[Tuple]] = None,
        rowcount:    int = -1,
        lastrowid:   Optional[int] = None,
    ) -> None:
        self.rows        = rows
        self.description = description
        self.rowcount    = rowcount
        self.lastrowid   = lastrowid
        self._pos        = 0

    def fetchone(self) -> Optional[Tuple]:
        if self._pos >= len(self.rows):
            return None
        row = self.rows[self._pos]
        self._pos += 1
        return row

    def fetchmany(self, size: int = 1) -> List[Tuple]:
        rows = self.rows[self._pos: self._pos + size]
        self._pos += len(rows)
        return rows

    def fetchall(self) -> List[Tuple]:
        rows = self.rows[self._pos:]
        self._pos = len(self.rows)
        return rows

    def __iter__(self):
        return iter(self.rows)


class MockSQLExecutor:

    def __init__(self, storage: Any, database: Optional[str] = None,
                 defaults: Optional[dict] = None) -> None:
        self._storage = storage
        self._defaults = defaults
        conn = get_connection_defaults(defaults)
        self._database = database or conn.get("default_database", "mock")
        self._schemas: Dict[str, List[str]] = {}
        self._autoincrement: Dict[str, int] = {}

    def _coll(self, table: str) -> str:
        return f"sql_table:{self._database}:{table.lower()}"

    def execute_create_table(self, sql: str) -> MockQueryResult:
        m = re.search(
            r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`\"]?(\w+)[`\"]?\s*\((.+)\)",
            sql, re.IGNORECASE | re.DOTALL,
        )
        op = get_operation_defaults("create_table", self._defaults)
        if not m:
            return MockQueryResult([], rowcount=op.get("rowcount", 0))
        table = m.group(1).lower()
        cols  = [c.strip().split()[0].strip('`"') for c in m.group(2).split(",") if c.strip()]
        self._schemas[table] = cols
        return MockQueryResult([], rowcount=op.get("rowcount", 0))

    def execute_drop_table(self, sql: str) -> MockQueryResult:
        op = get_operation_defaults("drop_table", self._defaults)
        m = re.search(r"DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?[`\"]?(\w+)[`\"]?", sql, re.IGNORECASE)
        if m:
            table = m.group(1).lower()
            self._schemas.pop(table, None)
            coll  = self._coll(table)
            for rec in self._storage.nosql.list(coll):
                self._storage.nosql.delete(coll, rec["key"])
        return MockQueryResult([], rowcount=op.get("rowcount", 0))

    def execute_truncate(self, sql: str) -> MockQueryResult:
        op = get_operation_defaults("truncate", self._defaults)
        m = re.search(r"TRUNCATE\s+(?:TABLE\s+)?[`\"]?(\w+)[`\"]?", sql, re.IGNORECASE)
        if m:
            coll = self._coll(m.group(1).lower())
            for rec in self._storage.nosql.list(coll):
                self._storage.nosql.delete(coll, rec["key"])
        return MockQueryResult([], rowcount=op.get("rowcount", 0))

    def execute_alter_table(self, sql: str) -> MockQueryResult:
        op = get_operation_defaults("alter_table", self._defaults)
        m = re.search(
            r"ALTER\s+TABLE\s+[`\"]?(\w+)[`\"]?\s+ADD\s+(?:COLUMN\s+)?[`\"]?(\w+)[`\"]?",
            sql, re.IGNORECASE,
        )
        if m:
            table = m.group(1).lower()
            col   = m.group(2)
            if table in self._schemas and col not in self._schemas[table]:
                self._schemas[table].append(col)
        return MockQueryResult([], rowcount=op.get("rowcount", 0))

    def execute_insert(self, sql: str, params: Any = None) -> MockQueryResult:
        op = get_operation_defaults("insert", self._defaults)
        m = re.search(
            r"INSERT\s+(?:INTO\s+)?[`\"]?(\w+)[`\"]?\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)",
            sql, re.IGNORECASE,
        )
        if not m:
            return MockQueryResult([], rowcount=op.get("rowcount", 0))

        table  = m.group(1).lower()
        cols   = [c.strip().strip('`"') for c in m.group(2).split(",")]
        val_str = m.group(3)

        values = _resolve_params(val_str, params)

        row_dict = dict(zip(cols, values))

        start_id = op.get("lastrowid", 1) - 1
        self._autoincrement[table] = self._autoincrement.get(table, start_id) + 1
        row_id = self._autoincrement[table]
        if "id" not in row_dict:
            row_dict["id"] = row_id

        coll = self._coll(table)
        self._storage.nosql.put(coll, str(row_id), row_dict)
        return MockQueryResult([], rowcount=op.get("rowcount", 1), lastrowid=row_id)

    def execute_update(self, sql: str, params: Any = None) -> MockQueryResult:
        m = re.search(
            r"UPDATE\s+[`\"]?(\w+)[`\"]?\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$",
            sql, re.IGNORECASE,
        )
        if not m:
            return MockQueryResult([], rowcount=0)

        table      = m.group(1).lower()
        set_clause = m.group(2).strip()
        where      = m.group(3)
        coll       = self._coll(table)
        records    = self._storage.nosql.list(coll)

        assignments = {}
        param_list = list(params) if params else []
        param_idx = 0
        for part in re.split(r",\s*(?=\w+\s*=)", set_clause):
            kv = part.split("=", 1)
            if len(kv) == 2:
                col = kv[0].strip().strip('`"')
                raw_val = kv[1].strip()
                if raw_val in ("%s", "?") or re.match(r"^\$\d+$", raw_val):
                    assignments[col] = param_list[param_idx] if param_idx < len(param_list) else None
                    param_idx += 1
                else:
                    assignments[col] = _coerce(raw_val.strip("'\""))

        # Remaining params are for the WHERE clause
        where_params = param_list[param_idx:] if param_list else None

        updated = 0
        for rec in records:
            if where and not _eval_where(rec["value"], where, where_params):
                continue
            rec["value"].update(assignments)
            self._storage.nosql.put(coll, rec["key"], rec["value"])
            updated += 1
        return MockQueryResult([], rowcount=updated)

    def execute_delete(self, sql: str, params: Any = None) -> MockQueryResult:
        m = re.search(
            r"DELETE\s+FROM\s+[`\"]?(\w+)[`\"]?(?:\s+WHERE\s+(.+))?$",
            sql, re.IGNORECASE,
        )
        if not m:
            return MockQueryResult([], rowcount=0)

        table   = m.group(1).lower()
        where   = m.group(2)
        coll    = self._coll(table)
        records = self._storage.nosql.list(coll)
        deleted = 0
        for rec in records:
            if where and not _eval_where(rec["value"], where, params):
                continue
            self._storage.nosql.delete(coll, rec["key"])
            deleted += 1
        return MockQueryResult([], rowcount=deleted)

    def execute_select(self, sql: str, params: Any = None) -> MockQueryResult:
        sql_clean = re.sub(r"\s+", " ", sql.strip())

        count_m = re.search(
            r"SELECT\s+COUNT\s*\(\s*\*\s*\)\s+FROM\s+[`\"]?(\w+)[`\"]?(?:\s+WHERE\s+(.+?))?(?:\s+ORDER|\s+LIMIT|$)",
            sql_clean, re.IGNORECASE,
        )
        if count_m:
            table   = count_m.group(1).lower()
            where   = count_m.group(2)
            records = self._storage.nosql.list(self._coll(table))
            rows    = [r["value"] for r in records
                       if not where or _eval_where(r["value"], where, params)]
            desc    = [("count", None, None, None, None, None, None)]
            return MockQueryResult([(len(rows),)], description=desc, rowcount=len(rows))

        m = re.search(
            r"SELECT\s+(.+?)\s+FROM\s+[`\"]?(\w+)[`\"]?"
            r"(?:\s+WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(.+?))?(?:\s+LIMIT\s+(\d+))?$",
            sql_clean, re.IGNORECASE,
        )
        if not m:
            return MockQueryResult([])

        col_str = m.group(1).strip()
        table   = m.group(2).lower()
        where   = m.group(3)
        order   = m.group(4)
        limit   = int(m.group(5)) if m.group(5) else None

        select_cols = (
            None if col_str == "*"
            else [c.strip().strip('`"').split(".")[-1] for c in col_str.split(",")]
        )

        records = self._storage.nosql.list(self._coll(table))
        rows    = [r["value"] for r in records
                   if not where or _eval_where(r["value"], where, params)]

        if order:
            order_clean = order.strip()
            desc_order  = "DESC" in order_clean.upper()
            order_col   = re.sub(r"\s+(ASC|DESC)\s*$", "", order_clean, flags=re.IGNORECASE).strip().strip('`"')
            rows.sort(key=lambda r: r.get(order_col, ""), reverse=desc_order)

        if limit is not None:
            rows = rows[:limit]

        if select_cols:
            result_rows = [tuple(r.get(c) for c in select_cols) for r in rows]
            description = [(c, None, None, None, None, None, None) for c in select_cols]
        else:
            all_cols    = self._schemas.get(table) or (list(rows[0].keys()) if rows else [])
            result_rows = [tuple(r.get(c) for c in all_cols) for r in rows]
            description = [(c, None, None, None, None, None, None) for c in all_cols]

        return MockQueryResult(result_rows, description=description, rowcount=len(result_rows))

    def execute(self, sql: str, params: Any = None) -> MockQueryResult:
        sql_stripped = sql.strip()
        verb = sql_stripped.split()[0].upper() if sql_stripped else ""

        if verb == "SELECT":
            return self.execute_select(sql_stripped, params)
        if verb == "INSERT":
            return self.execute_insert(sql_stripped, params)
        if verb == "UPDATE":
            return self.execute_update(sql_stripped, params)
        if verb == "DELETE":
            return self.execute_delete(sql_stripped, params)
        if verb == "CREATE":
            return self.execute_create_table(sql_stripped)
        if verb == "DROP":
            return self.execute_drop_table(sql_stripped)
        if verb in ("TRUNCATE",):
            return self.execute_truncate(sql_stripped)
        if verb == "ALTER":
            return self.execute_alter_table(sql_stripped)
        if verb == "CALL":
            op = get_operation_defaults("callproc", self._defaults)
            return MockQueryResult(
                op.get("rows", []),
                rowcount=op.get("rowcount", 0),
            )
        return MockQueryResult([], rowcount=0)


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _resolve_params(val_str: str, params: Any) -> List[Any]:
    raw_vals = [v.strip() for v in _split_values(val_str)]
    if not params:
        return [_coerce(v) for v in raw_vals]

    param_list = list(params) if not isinstance(params, (list, tuple)) else list(params)
    result     = []
    idx        = 0
    for v in raw_vals:
        if v in ("%s", "?") or re.match(r"^\$\d+$", v):
            result.append(param_list[idx] if idx < len(param_list) else None)
            idx += 1
        else:
            result.append(_coerce(v))
    return result


def _split_values(val_str: str) -> List[str]:
    parts, current, depth = [], [], 0
    in_quote, quote_char  = False, None
    for ch in val_str:
        if in_quote:
            current.append(ch)
            if ch == quote_char:
                in_quote = False
        elif ch in ("'", '"'):
            in_quote, quote_char = True, ch
            current.append(ch)
        elif ch == "," and depth == 0:
            parts.append("".join(current).strip())
            current = []
        else:
            current.append(ch)
    if current:
        parts.append("".join(current).strip())
    return parts


def _coerce(val: str) -> Any:
    v = val.strip()
    if v.upper() == "NULL":
        return None
    if v.upper() in ("TRUE", "FALSE"):
        return v.upper() == "TRUE"
    if (v.startswith("'") and v.endswith("'")) or (v.startswith('"') and v.endswith('"')):
        return v[1:-1]
    try:
        return int(v)
    except ValueError:
        pass
    try:
        return float(v)
    except ValueError:
        pass
    return v


def _eval_where(row: Dict[str, Any], where: str, params: Any = None) -> bool:
    if not where:
        return True

    where_clean = where.strip()

    and_parts = re.split(r"\bAND\b", where_clean, flags=re.IGNORECASE)
    results   = []
    param_idx = [0]
    param_list = list(params) if params else []

    for part in and_parts:
        or_parts = re.split(r"\bOR\b", part, flags=re.IGNORECASE)
        any_true = False
        for clause in or_parts:
            if _eval_clause(clause.strip(), row, param_list, param_idx):
                any_true = True
        results.append(any_true)

    return all(results)


def _eval_clause(clause: str, row: Dict[str, Any],
                 params: List[Any], idx: List[int]) -> bool:
    m = re.match(r"[`\"]?(\w+)[`\"]?\s+IS\s+(NOT\s+)?NULL", clause, re.IGNORECASE)
    if m:
        val = row.get(m.group(1).strip('`"'))
        return (val is None) if not m.group(2) else (val is not None)

    m = re.match(r"[`\"]?(\w+)[`\"]?\s+LIKE\s+'([^']*)'", clause, re.IGNORECASE)
    if m:
        col, pattern = m.group(1), m.group(2)
        regex = "^" + re.escape(pattern).replace(r"\%", ".*").replace(r"\_", ".") + "$"
        return bool(re.match(regex, str(row.get(col, "")), re.IGNORECASE))

    m = re.match(r"[`\"]?(\w+)[`\"]?\s+IN\s*\(([^)]+)\)", clause, re.IGNORECASE)
    if m:
        col    = m.group(1)
        in_vals = [_coerce(v.strip()) for v in m.group(2).split(",")]
        return row.get(col) in in_vals

    m = re.match(
        r"[`\"]?(\w+)[`\"]?\s*(=|!=|<>|<=|>=|<|>)\s*(.+)$",
        clause, re.IGNORECASE,
    )
    if not m:
        return True

    col = m.group(1).strip('`"')
    op  = m.group(2)
    rhs = m.group(3).strip()

    if rhs in ("%s", "?") or re.match(r"^\$\d+$", rhs):
        rhs_val = params[idx[0]] if idx[0] < len(params) else None
        idx[0] += 1
    else:
        rhs_val = _coerce(rhs)

    lhs_val = row.get(col)

    try:
        if op == "=":   return lhs_val == rhs_val
        if op in ("!=", "<>"): return lhs_val != rhs_val
        if op == "<":   return lhs_val <  rhs_val
        if op == ">":   return lhs_val >  rhs_val
        if op == "<=":  return lhs_val <= rhs_val
        if op == ">=":  return lhs_val >= rhs_val
    except TypeError:
        return False
    return False
