"""Autonomous Hierarchy Detection — P1.4.

Four detection algorithms to find hierarchy structures in table columns:

1. **Self-referential FK** — ``*_ID`` + ``*_PARENT_ID`` in same table.
2. **LEVEL_N columns** — regex match ``LEVEL_\\d+``, ``LVL_\\d+``.
3. **Naming patterns** — ``*_GROUP``, ``*_CATEGORY``, ``*_TYPE``, ``*_CLASS``
   columns indicate hierarchy roll-ups.
4. **Related table chains** — DIM→DIM FK chains (e.g.,
   DIM_ACCOUNT → DIM_ACCOUNT_GROUP → DIM_COA_GROUP).
"""
from __future__ import annotations

import re
from typing import Dict, List, Optional


# ---------------------------------------------------------------------------
# Patterns
# ---------------------------------------------------------------------------

_SELF_REF_PARENT = re.compile(r"^(.+?)_?PARENT_?(?:ID|HID|KEY|CODE|NUM)$", re.IGNORECASE)
_SELF_REF_ID = re.compile(r"^(.+?)_?(?:ID|HID|KEY|CODE|NUM)$", re.IGNORECASE)
_LEVEL_N = re.compile(r"^(?:LEVEL|LVL)[_]?(\d+)(?:_.*)?$", re.IGNORECASE)
_HIERARCHY_SUFFIX = re.compile(
    r"_(?:GROUP|CATEGORY|TYPE|CLASS|SUBTYPE|SUBGROUP|SUBCATEGORY|TIER|RANK)$",
    re.IGNORECASE,
)
_KEY_SUFFIXES = ("_id", "_hid", "_key", "_code", "_num", "_tid", "_sk")


def _is_key_col(col: str) -> bool:
    """Check if column name ends with a key suffix."""
    lower = col.lower()
    return any(lower.endswith(s) for s in _KEY_SUFFIXES)


def _entity_token(name: str) -> str:
    """Normalise name to a lowercase token without underscores."""
    return re.sub(r"[^a-z0-9]", "", name.lower())


def _strip_prefix(name: str) -> str:
    """Strip common warehouse prefixes (DIM_, FACT_, etc.)."""
    for pfx in ("DIM_", "FACT_", "XREF_", "BRIDGE_", "STG_", "RAW_"):
        if name.upper().startswith(pfx):
            return name[len(pfx):]
    return name


# ---------------------------------------------------------------------------
# Algorithm 1: Self-referential FK
# ---------------------------------------------------------------------------

def _detect_self_referential(columns: List[str]) -> List[str]:
    """Find parent-child column pairs in the same table.

    Handles both patterns:
    - ``ENTITY_PARENT_ID`` + ``ENTITY_ID``  (suffix-style)
    - ``PARENT_ENTITY_ID`` + ``ENTITY_ID``  (prefix-style)

    Returns hierarchy levels like ["ACCOUNT_ID", "PARENT_ACCOUNT_ID"].
    """
    cols_upper = [c.upper() for c in columns]
    cols_set = set(cols_upper)

    # Strategy 1: look for columns containing "PARENT"
    for col in cols_upper:
        if "PARENT" not in col:
            continue
        # Strip "PARENT" and underscores to get the entity+suffix
        stripped = col.replace("PARENT_", "").replace("_PARENT", "").replace("PARENT", "")
        stripped = stripped.strip("_")
        if not stripped:
            continue
        # Check if the stripped version exists as a sibling column
        if stripped in cols_set and stripped != col:
            return [stripped, col]
        # Also check with underscore re-assembly
        for candidate in cols_upper:
            if candidate == col:
                continue
            if _entity_token(candidate) == _entity_token(stripped):
                return [candidate, col]

    return []


# ---------------------------------------------------------------------------
# Algorithm 2: LEVEL_N columns
# ---------------------------------------------------------------------------

def _detect_level_columns(columns: List[str]) -> List[str]:
    """Find LEVEL_1, LEVEL_2, ... columns and return them ordered.

    Returns sorted level column names.
    """
    level_map: Dict[int, str] = {}
    for col in columns:
        m = _LEVEL_N.match(col)
        if m:
            level_map[int(m.group(1))] = col
    if level_map:
        return [level_map[k] for k in sorted(level_map.keys())]
    return []


# ---------------------------------------------------------------------------
# Algorithm 3: Naming patterns (*_GROUP, *_CATEGORY, etc.)
# ---------------------------------------------------------------------------

def _detect_naming_patterns(table_name: str, columns: List[str]) -> List[str]:
    """Find columns with hierarchy-indicating suffixes.

    Sorts by specificity: base entity first, then GROUP/CATEGORY/TYPE/CLASS.
    Returns hierarchy from most specific (leaf) to least specific (root).
    """
    entity = _strip_prefix(table_name).upper()
    hierarchy_cols: List[str] = []

    for col in columns:
        if _HIERARCHY_SUFFIX.search(col):
            hierarchy_cols.append(col.upper())

    if not hierarchy_cols:
        return []

    # Sort: columns containing the entity token come first (most specific),
    # then alphabetically
    entity_token = _entity_token(entity)

    def sort_key(c: str) -> tuple:
        ct = _entity_token(c)
        has_entity = entity_token in ct
        # Rank by suffix type: TYPE > SUBTYPE > GROUP > CATEGORY > CLASS
        suffix_rank = 5
        cl = c.lower()
        if "_subtype" in cl or "_subgroup" in cl or "_subcategory" in cl:
            suffix_rank = 1
        elif "_type" in cl:
            suffix_rank = 2
        elif "_class" in cl:
            suffix_rank = 3
        elif "_category" in cl:
            suffix_rank = 4
        elif "_group" in cl:
            suffix_rank = 5
        return (0 if has_entity else 1, suffix_rank, c)

    hierarchy_cols.sort(key=sort_key)
    return hierarchy_cols


# ---------------------------------------------------------------------------
# Algorithm 4: Related table chains (FK-based)
# ---------------------------------------------------------------------------

def _detect_table_chains(
    table_name: str,
    relationships: List[dict],
) -> List[str]:
    """Follow FK relationships to find DIM→DIM chains.

    Starting from ``table_name``, follows outgoing FKs to other tables
    that look like dimensions to build a chain.

    Returns chain like ["DIM_ACCOUNT", "DIM_ACCOUNT_GROUP", "DIM_COA_GROUP"].
    """
    if not relationships:
        return []

    # Build adjacency: from_table → [to_table, ...]
    adj: Dict[str, List[str]] = {}
    for rel in relationships:
        src = (rel.get("from_table", "") or rel.get("source_table", "")).upper()
        tgt = (rel.get("to_table", "") or rel.get("target_table", "")).upper()
        if src and tgt and src != tgt:
            adj.setdefault(src, []).append(tgt)

    start = table_name.upper()
    chain = [start]
    visited = {start}
    current = start

    # Follow single-link chains (stop at branches or cycles)
    for _ in range(10):  # safety limit
        targets = adj.get(current, [])
        # Filter to dimension-like targets not yet visited
        dim_targets = [
            t for t in targets
            if t not in visited and (t.startswith("DIM_") or t.startswith("XREF_"))
        ]
        if len(dim_targets) == 1:
            nxt = dim_targets[0]
            chain.append(nxt)
            visited.add(nxt)
            current = nxt
        else:
            break

    return chain if len(chain) >= 2 else []


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def detect_hierarchies(
    table_columns: Dict[str, List[str]],
    relationships: Optional[List[dict]] = None,
) -> Dict[str, List[str]]:
    """Detect hierarchy structures across all tables.

    Runs all four algorithms on each table and merges results.
    Only returns tables where at least one hierarchy was found.

    Args:
        table_columns: {table_name: [column_name, ...]} for each table.
        relationships: Optional FK relationship dicts with
                       from_table/to_table or source_table/target_table.

    Returns:
        {table_name: [level_1, level_2, ...]} — hierarchy levels from
        most specific (leaf) to least specific (root).
    """
    result: Dict[str, List[str]] = {}

    for table, columns in table_columns.items():
        table_upper = table.upper()
        levels: List[str] = []

        # 1. Self-referential
        sr = _detect_self_referential(columns)
        if sr:
            levels = sr

        # 2. LEVEL_N columns (overrides self-ref if found)
        lvl = _detect_level_columns(columns)
        if lvl:
            levels = lvl

        # 3. Naming patterns (merge if no other hierarchy found)
        naming = _detect_naming_patterns(table, columns)
        if naming and not levels:
            levels = naming

        # 4. Related table chains
        if relationships:
            chain = _detect_table_chains(table, relationships)
            if chain and len(chain) > len(levels):
                levels = chain

        if levels:
            result[table_upper] = levels

    return result
