from __future__ import annotations

import re
from copy import deepcopy
from typing import Any, Callable

SourceResolver = Callable[[dict[str, Any]], Any]

LIST_KEYS = (
    "rows", "players", "items", "data", "results", "scores", "tournaments",
    "rounds", "list", "records", "entries",
)


def _safe_text(value: Any) -> str:
    if value is None:
        return ""
    return str(value)


def get_nested_value(obj: Any, path: Any, default: Any = "") -> Any:
    """Read values by direct key, dotted path, or bracket-ish path.

    Examples:
    - player_id
    - raw.player.id
    - holes.0.score
    - holes[0].score
    """
    if path in (None, ""):
        return default
    key = str(path).strip()
    if not key:
        return default

    if isinstance(obj, dict) and key in obj:
        value = obj.get(key)
        return default if value is None else value

    tokens = [p for p in re.split(r"\.|\[|\]", key.replace("/", ".")) if p not in ("", None)]
    cur = obj
    for token in tokens:
        if isinstance(cur, dict):
            if token in cur:
                cur = cur[token]
            else:
                return default
        elif isinstance(cur, list):
            try:
                cur = cur[int(token)]
            except Exception:
                return default
        else:
            return default
    return default if cur is None else cur


def flatten_dict(obj: Any, prefix: str = "") -> dict[str, Any]:
    out: dict[str, Any] = {}
    if isinstance(obj, dict):
        for key, value in obj.items():
            clean_key = str(key)
            next_prefix = f"{prefix}_{clean_key}" if prefix else clean_key
            if isinstance(value, dict):
                out.update(flatten_dict(value, next_prefix))
            elif isinstance(value, list):
                if all(not isinstance(x, (dict, list)) for x in value):
                    out[next_prefix] = " | ".join(_safe_text(x) for x in value)
                else:
                    out[next_prefix] = value
            else:
                out[next_prefix] = value
    return out


def as_rows(data: Any) -> list[dict[str, Any]]:
    """Convert almost any JSON response into a list of row dictionaries."""
    candidate: Any = data
    if isinstance(data, dict):
        for key in LIST_KEYS:
            value = data.get(key)
            if isinstance(value, list):
                candidate = value
                break
        else:
            # Fallback: choose the largest list of dicts in the object.
            lists: list[list[Any]] = []

            def walk(obj: Any) -> None:
                if isinstance(obj, dict):
                    for value in obj.values():
                        walk(value)
                elif isinstance(obj, list) and obj and all(isinstance(x, dict) for x in obj):
                    lists.append(obj)

            walk(data)
            if lists:
                candidate = max(lists, key=len)
            else:
                candidate = [data]

    if isinstance(candidate, dict):
        candidate = [candidate]
    if not isinstance(candidate, list):
        return []

    rows: list[dict[str, Any]] = []
    for item in candidate:
        if isinstance(item, dict):
            flat = flatten_dict(item)
            row = dict(item)
            for key, value in flat.items():
                row.setdefault(key, value)
            rows.append(row)
        else:
            rows.append({"value": item})
    return rows


def _normalize_match_value(value: Any, *, case_sensitive: bool = False, trim: bool = True) -> str:
    text = _safe_text(value)
    if trim:
        text = text.strip()
    if not case_sensitive:
        text = text.lower()
    return text


def _match_pairs(join: dict[str, Any]) -> list[dict[str, str]]:
    pairs = join.get("match") or join.get("matches") or []
    if isinstance(pairs, list) and pairs:
        result = []
        for item in pairs:
            if isinstance(item, dict):
                left = item.get("left") or item.get("base") or item.get("from") or item.get("local")
                right = item.get("right") or item.get("source") or item.get("to") or item.get("remote")
                if left and right:
                    result.append({"left": str(left), "right": str(right)})
        if result:
            return result

    left_keys = join.get("left_keys") or join.get("left") or join.get("base_keys") or ""
    right_keys = join.get("right_keys") or join.get("right") or join.get("source_keys") or ""
    if isinstance(left_keys, str):
        left_list = [x.strip() for x in re.split(r"[,;\n]+", left_keys) if x.strip()]
    else:
        left_list = [str(x).strip() for x in left_keys or [] if str(x).strip()]
    if isinstance(right_keys, str):
        right_list = [x.strip() for x in re.split(r"[,;\n]+", right_keys) if x.strip()]
    else:
        right_list = [str(x).strip() for x in right_keys or [] if str(x).strip()]
    return [{"left": l, "right": r} for l, r in zip(left_list, right_list)]


def _field_rules(join: dict[str, Any]) -> list[dict[str, Any]]:
    fields = join.get("fields") or join.get("output_fields") or []
    if isinstance(fields, str):
        parsed: list[dict[str, Any]] = []
        for line in re.split(r"[\n;]+", fields):
            line = line.strip()
            if not line:
                continue
            if "=" in line:
                key, source = line.split("=", 1)
            elif ":" in line:
                key, source = line.split(":", 1)
            else:
                key, source = line, line
            parsed.append({"key": key.strip(), "source": source.strip()})
        fields = parsed
    result: list[dict[str, Any]] = []
    for field in fields:
        if isinstance(field, str):
            result.append({"key": field, "source": field})
        elif isinstance(field, dict):
            key = field.get("key") or field.get("name") or field.get("target") or field.get("output")
            source = field.get("source") or field.get("from") or field.get("path") or key
            if key:
                result.append({
                    "key": str(key),
                    "source": str(source or key),
                    "default": field.get("default", ""),
                    "enabled": field.get("enabled", True),
                })
    return result


def _row_key(row: dict[str, Any], fields: list[str], *, case_sensitive: bool, trim: bool) -> tuple[str, ...]:
    return tuple(_normalize_match_value(get_nested_value(row, field), case_sensitive=case_sensitive, trim=trim) for field in fields)


def apply_join_rules(
    base_rows: list[dict[str, Any]],
    joins: list[dict[str, Any]] | None,
    source_resolver: SourceResolver,
) -> list[dict[str, Any]]:
    """Merge data from other JSON sources into base rows.

    Config example::

        {
          "enabled": true,
          "title": "Подтянуть регион",
          "source_type": "url",              # scores | tournament | round | vmix_json | url | file
          "source_key": "https://.../x.json", # or another vMix JSON key
          "match": [{"left": "player_id", "right": "id"}],
          "fields": [{"key": "region", "source": "region"}],
          "multiple": "first",              # first | last | all | count
          "separator": " | ",
          "prefix": "",
          "case_sensitive": false,
          "trim": true
        }
    """
    if not joins:
        return base_rows

    result = [dict(row) for row in base_rows]

    for join in joins:
        if not isinstance(join, dict) or join.get("enabled", True) is False:
            continue
        pairs = _match_pairs(join)
        if not pairs:
            continue
        fields = _field_rules(join)
        mode = str(join.get("multiple") or join.get("mode") or "first").lower()
        separator = _safe_text(join.get("separator", " | "))
        prefix = _safe_text(join.get("prefix", ""))
        case_sensitive = bool(join.get("case_sensitive", False))
        trim = bool(join.get("trim", True))

        try:
            source_rows = as_rows(source_resolver(join))
        except Exception as exc:
            if join.get("required"):
                raise
            # Keep a lightweight error field for debugging without breaking vMix.
            error_key = prefix + _safe_text(join.get("error_key") or "join_error")
            for row in result:
                row.setdefault(error_key, str(exc))
            continue

        if not source_rows:
            continue

        right_fields = [p["right"] for p in pairs]
        left_fields = [p["left"] for p in pairs]
        index: dict[tuple[str, ...], list[dict[str, Any]]] = {}
        for source_row in source_rows:
            key = _row_key(source_row, right_fields, case_sensitive=case_sensitive, trim=trim)
            # Empty composite keys are usually bad matches; skip them.
            if not any(key):
                continue
            index.setdefault(key, []).append(source_row)

        count_key = prefix + _safe_text(join.get("count_key") or "join_count")
        for row in result:
            key = _row_key(row, left_fields, case_sensitive=case_sensitive, trim=trim)
            matches = index.get(key, []) if any(key) else []

            if mode == "count":
                row[count_key] = len(matches)
                continue
            if not matches:
                for field in fields:
                    if field.get("enabled", True) is not False:
                        row.setdefault(prefix + field["key"], field.get("default", ""))
                continue

            selected_rows = matches
            if mode == "last":
                selected_rows = [matches[-1]]
            elif mode not in {"all", "list"}:
                selected_rows = [matches[0]]

            for field in fields:
                if field.get("enabled", True) is False:
                    continue
                out_key = prefix + field["key"]
                source_path = field.get("source") or field["key"]
                default = field.get("default", "")
                values = [get_nested_value(src, source_path, default) for src in selected_rows]
                if mode in {"all", "list"}:
                    row[out_key] = separator.join(_safe_text(v) for v in values if v not in (None, ""))
                else:
                    row[out_key] = values[0] if values and values[0] not in (None, "") else default

    return result


def compact_join_for_storage(join: dict[str, Any]) -> dict[str, Any]:
    """Normalize UI payload before saving it to portable config."""
    data = deepcopy(join)
    data.setdefault("enabled", True)
    data.setdefault("source_type", "scores")
    data.setdefault("source_key", "")
    data.setdefault("multiple", "first")
    data.setdefault("separator", " | ")
    data.setdefault("prefix", "")
    data.setdefault("case_sensitive", False)
    data.setdefault("trim", True)
    data["match"] = _match_pairs(data)
    data["fields"] = _field_rules(data)
    return data
