"""Snowflake-specific lineage extraction MCP tool."""

from __future__ import annotations

import csv
import json
from collections import defaultdict, deque
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Iterable, List, Set, Tuple


def _load_toml(path: Path) -> Dict[str, Any]:
    try:
        import tomllib  # type: ignore
        return tomllib.loads(path.read_text(encoding="utf-8"))
    except Exception:
        try:
            import tomli  # type: ignore
            return tomli.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {}


def _parse_fqn(target_table: str) -> Tuple[str, str, str]:
    parts = [p.strip().strip('"') for p in target_table.split(".") if p.strip()]
    if len(parts) != 3:
        raise ValueError("target_table must be DATABASE.SCHEMA.OBJECT")
    return parts[0].upper(), parts[1].upper(), parts[2].upper()


def _safe_ident(value: str) -> str:
    return '"' + str(value).replace('"', '""') + '"'


def _to_fqn(database: str, schema: str, name: str) -> str:
    return f"{database}.{schema}.{name}"


def _quoted_fqn(database: str, schema: str, name: str) -> str:
    return f"{_safe_ident(database)}.{_safe_ident(schema)}.{_safe_ident(name)}"


def _safe_filename(value: str) -> str:
    keep = []
    for ch in value:
        if ch.isalnum() or ch in ("_", ".", "-"):
            keep.append(ch)
        else:
            keep.append("_")
    return "".join(keep)


def _write_json(path: Path, payload: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, default=str), encoding="utf-8")


def _write_csv(path: Path, headers: List[str], rows: Iterable[Iterable[Any]]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", newline="", encoding="utf-8") as fh:
        writer = csv.writer(fh)
        writer.writerow(headers)
        for row in rows:
            writer.writerow([str(v) if v is not None else "" for v in row])


def _read_connection(config_file: str, connection_name: str) -> Dict[str, Any]:
    candidates = [Path(config_file)]
    candidates.append(Path.home() / ".snowflake" / "config.toml")
    candidates.append(Path.home() / ".snowflake" / "connections.toml")

    for path in candidates:
        if not path.exists():
            continue
        cfg = _load_toml(path)
        conns = cfg.get("connections", {}) if isinstance(cfg, dict) else {}
        if not isinstance(conns, dict):
            continue
        picked = conns.get(connection_name)
        if isinstance(picked, dict):
            return dict(picked)
    return {}


def _connect(conn_cfg: Dict[str, Any]):
    try:
        import snowflake.connector
    except Exception as exc:
        raise RuntimeError(f"snowflake-connector-python unavailable: {exc}") from exc

    kwargs: Dict[str, Any] = {
        "account": conn_cfg.get("account"),
        "user": conn_cfg.get("user"),
        "password": conn_cfg.get("password"),
        "warehouse": conn_cfg.get("warehouse"),
        "database": conn_cfg.get("database"),
        "schema": conn_cfg.get("schema"),
        "role": conn_cfg.get("role"),
        "authenticator": conn_cfg.get("authenticator", "externalbrowser"),
        "client_store_temporary_credential": True,
        "client_request_mfa_token": True,
        "login_timeout": 30,
        "network_timeout": 60,
    }
    clean = {k: v for k, v in kwargs.items() if v not in (None, "")}
    return snowflake.connector.connect(**clean)


def _collect_object_refs(payload: Any) -> Set[Tuple[str, str, str, str]]:
    refs: Set[Tuple[str, str, str, str]] = set()

    def walk(node: Any) -> None:
        if isinstance(node, dict):
            name = str(node.get("objectName", "")).strip()
            domain = str(node.get("objectDomain", "")).strip().upper()
            if name and domain:
                parts = [p.strip().strip('"') for p in name.split(".") if p.strip()]
                if len(parts) == 3:
                    refs.add((parts[0].upper(), parts[1].upper(), parts[2].upper(), domain))
            for v in node.values():
                walk(v)
        elif isinstance(node, list):
            for item in node:
                walk(item)

    walk(payload)
    return refs


def _sql_literal(value: str) -> str:
    return "'" + str(value).replace("'", "''") + "'"


def _run_first_success(cur, queries: List[str]) -> Tuple[List[str], List[Tuple[Any, ...]], int]:
    last_exc: Exception | None = None
    for idx, sql in enumerate(queries):
        try:
            cur.execute(sql)
            headers = [d[0] for d in (cur.description or [])]
            return headers, cur.fetchall(), idx
        except Exception as exc:
            last_exc = exc
    if last_exc:
        raise last_exc
    return [], [], -1


def _get_ddl_with_fallback(cur, fqn: str, preferred_domains: List[str]) -> str:
    tried = []
    domains = preferred_domains + [d for d in ["TABLE", "VIEW", "DYNAMIC TABLE"] if d not in preferred_domains]
    last_exc: Exception | None = None
    for dom in domains:
        try:
            cur.execute(f"SELECT GET_DDL({_sql_literal(dom)}, {_sql_literal(fqn)})")
            row = cur.fetchone()
            if row and row[0]:
                return str(row[0])
        except Exception as exc:
            last_exc = exc
            tried.append(dom)
    if last_exc:
        raise RuntimeError(f"GET_DDL failed for {fqn}; tried {tried}: {last_exc}")
    raise RuntimeError(f"GET_DDL failed for {fqn}; tried {tried}")


def _fetch_transitive_dependency_edges(
    cur,
    seed_db: str,
    seed_schema: str,
    seed_obj: str,
    max_depth: int,
) -> List[Dict[str, str]]:
    """Fetch transitive dependency edges from OBJECT_DEPENDENCIES up to max depth."""
    frontier: Set[Tuple[str, str, str]] = {(seed_db, seed_schema, seed_obj)}
    visited_nodes: Set[Tuple[str, str, str]] = set(frontier)
    visited_edges: Set[Tuple[str, str, str, str, str, str]] = set()
    all_edges: List[Dict[str, str]] = []

    for _ in range(max_depth):
        if not frontier:
            break

        conds = []
        for fdb, fsch, fobj in sorted(frontier):
            conds.append(
                "("
                f"UPPER(REFERENCING_DATABASE) = {_sql_literal(fdb)} AND "
                f"UPPER(REFERENCING_SCHEMA) = {_sql_literal(fsch)} AND "
                f"UPPER(REFERENCING_OBJECT_NAME) = {_sql_literal(fobj)}"
                ")"
            )
            conds.append(
                "("
                f"UPPER(REFERENCED_DATABASE) = {_sql_literal(fdb)} AND "
                f"UPPER(REFERENCED_SCHEMA) = {_sql_literal(fsch)} AND "
                f"UPPER(REFERENCED_OBJECT_NAME) = {_sql_literal(fobj)}"
                ")"
            )
        where_clause = " OR ".join(conds) if conds else "1=0"
        sql = f"""
        SELECT
            UPPER(REFERENCING_DATABASE) AS REFERENCING_DATABASE,
            UPPER(REFERENCING_SCHEMA) AS REFERENCING_SCHEMA,
            UPPER(REFERENCING_OBJECT_NAME) AS REFERENCING_OBJECT_NAME,
            UPPER(REFERENCED_DATABASE) AS REFERENCED_DATABASE,
            UPPER(REFERENCED_SCHEMA) AS REFERENCED_SCHEMA,
            UPPER(REFERENCED_OBJECT_NAME) AS REFERENCED_OBJECT_NAME
        FROM SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES
        WHERE {where_clause}
        """
        cur.execute(sql)
        rows = cur.fetchall()

        next_frontier: Set[Tuple[str, str, str]] = set()
        for row in rows:
            rdb, rsch, robj, edb, esch, eobj = [str(v).upper() for v in row]
            edge_key = (rdb, rsch, robj, edb, esch, eobj)
            if edge_key in visited_edges:
                continue
            visited_edges.add(edge_key)
            all_edges.append(
                {
                    "referencing": f"{rdb}.{rsch}.{robj}",
                    "referenced": f"{edb}.{esch}.{eobj}",
                    "referencing_name": robj,
                    "referenced_name": eobj,
                }
            )
            for node in ((rdb, rsch, robj), (edb, esch, eobj)):
                if node not in visited_nodes:
                    visited_nodes.add(node)
                    next_frontier.add(node)
        frontier = next_frontier

    return all_edges


def _compute_dependency_levels(
    edges: List[Dict[str, str]],
    target_fqn: str,
) -> Dict[str, Any]:
    """Compute upstream/downstream depth levels from target object."""
    target = str(target_fqn).upper()
    upstream_adj: Dict[str, Set[str]] = defaultdict(set)
    downstream_adj: Dict[str, Set[str]] = defaultdict(set)
    all_nodes: Set[str] = {target}

    for edge in edges:
        refing = str(edge.get("referencing", "")).upper()
        refed = str(edge.get("referenced", "")).upper()
        if not refing or not refed:
            continue
        upstream_adj[refing].add(refed)
        downstream_adj[refed].add(refing)
        all_nodes.add(refing)
        all_nodes.add(refed)

    def bfs_levels(adj: Dict[str, Set[str]]) -> Dict[str, int]:
        q: deque[Tuple[str, int]] = deque([(target, 0)])
        depth_by_node: Dict[str, int] = {target: 0}
        while q:
            node, depth = q.popleft()
            for nxt in sorted(adj.get(node, set())):
                nd = depth + 1
                if nxt not in depth_by_node or nd < depth_by_node[nxt]:
                    depth_by_node[nxt] = nd
                    q.append((nxt, nd))
        return depth_by_node

    up_depth = bfs_levels(upstream_adj)
    down_depth = bfs_levels(downstream_adj)

    def to_levels(depth_map: Dict[str, int]) -> List[Dict[str, Any]]:
        levels: Dict[int, List[str]] = defaultdict(list)
        for node, depth in depth_map.items():
            if depth == 0:
                continue
            levels[depth].append(node.split(".")[-1])
        return [{"depth": d, "objects": sorted(set(levels[d]))} for d in sorted(levels)]

    objects: List[Dict[str, Any]] = []
    for node in sorted(all_nodes):
        up = up_depth.get(node)
        down = down_depth.get(node)
        if node == target:
            direction = "target"
            min_depth = 0
        elif up is not None and down is not None:
            direction = "both"
            min_depth = min(up, down)
        elif up is not None:
            direction = "upstream"
            min_depth = up
        elif down is not None:
            direction = "downstream"
            min_depth = down
        else:
            direction = "unknown"
            min_depth = None
        objects.append(
            {
                "name": node.split(".")[-1],
                "fqn": node,
                "direction": direction,
                "min_depth": min_depth,
            }
        )

    return {
        "target": target.split(".")[-1],
        "target_fqn": target,
        "upstream_levels": to_levels(up_depth),
        "downstream_levels": to_levels(down_depth),
        "objects": objects,
    }


def register_snowflake_lineage_tools(mcp, settings) -> Dict[str, Any]:
    """Register Snowflake lineage tool."""

    @mcp.tool()
    def snowflake_extract_lineage_bundle(
        target_table: str,
        connection_name: str = "default",
        output_dir: str = "",
        sample_limit: int = 1000,
        lookback_days: int = 14,
        max_dependency_depth: int = 5,
        export_ddls_and_samples: bool = True,
        config_file: str = ".snowflake_cli/config.toml",
        role_override: str = "",
        warehouse_override: str = "",
        include_task_refs: bool = False,
    ) -> str:
        """Extract Snowflake lineage evidence + DDL + top-N samples for target table.

        Snowflake-specific tool.
        """
        try:
            db, sch, obj = _parse_fqn(target_table)
            limit = max(1, min(int(sample_limit), 1000))
            lookback = max(1, min(int(lookback_days), 90))
            max_depth = max(1, min(int(max_dependency_depth), 12))
        except Exception as exc:
            return json.dumps({"error": str(exc)})

        base_dir = Path(output_dir) if output_dir else Path(getattr(settings, "data_dir", "data"))
        ts = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
        bundle_dir = base_dir / f"snowflake_lineage_{db}_{sch}_{obj}_{ts}"
        ddls_dir = bundle_dir / "ddls"
        samples_dir = bundle_dir / "samples"
        bundle_dir.mkdir(parents=True, exist_ok=True)
        ddls_dir.mkdir(parents=True, exist_ok=True)
        samples_dir.mkdir(parents=True, exist_ok=True)

        summary: Dict[str, Any] = {
            "target_table": target_table,
            "connection_name": connection_name,
            "bundle_dir": str(bundle_dir),
            "lookback_days": lookback,
            "sample_limit": limit,
            "max_dependency_depth": max_depth,
            "export_ddls_and_samples": bool(export_ddls_and_samples),
            "role_override": role_override,
            "warehouse_override": warehouse_override,
            "include_task_refs": bool(include_task_refs),
            "errors": [],
            "detailed_errors": [],
        }
        _write_json(bundle_dir / "00_input.json", summary)

        conn_cfg = _read_connection(config_file=config_file, connection_name=connection_name)
        if not conn_cfg:
            summary["errors"].append("connection_not_found")
            _write_json(bundle_dir / "99_summary.json", summary)
            return json.dumps(summary, indent=2, default=str)

        conn = None
        try:
            conn = _connect(conn_cfg)
            cur = conn.cursor()

            if role_override.strip():
                try:
                    cur.execute(f"USE ROLE {_safe_ident(role_override.strip().upper())}")
                except Exception as exc:
                    summary["errors"].append(f"use_role_failed:{role_override}:{exc}")
                    summary["detailed_errors"].append(
                        {
                            "category": "use_role",
                            "target": role_override,
                            "error": str(exc),
                        }
                    )

            if warehouse_override.strip():
                try:
                    cur.execute(f"USE WAREHOUSE {_safe_ident(warehouse_override.strip().upper())}")
                except Exception as exc:
                    summary["errors"].append(f"use_warehouse_failed:{warehouse_override}:{exc}")
                    summary["detailed_errors"].append(
                        {
                            "category": "use_warehouse",
                            "target": warehouse_override,
                            "error": str(exc),
                        }
                    )

            # Target DDL (domain fallback).
            target_ddl = _get_ddl_with_fallback(
                cur,
                _to_fqn(db, sch, obj),
                preferred_domains=["TABLE"],
            )
            (bundle_dir / "01_target_ddl.sql").write_text(str(target_ddl), encoding="utf-8")

            dep_rows: List[Tuple[Any, ...]] = []
            dep_headers: List[str] = []
            try:
                dep_sql = f"""
                SELECT
                    REFERENCING_OBJECT_DOMAIN,
                    REFERENCING_DATABASE,
                    REFERENCING_SCHEMA,
                    REFERENCING_OBJECT_NAME,
                    REFERENCED_OBJECT_DOMAIN,
                    REFERENCED_DATABASE,
                    REFERENCED_SCHEMA,
                    REFERENCED_OBJECT_NAME
                FROM SNOWFLAKE.ACCOUNT_USAGE.OBJECT_DEPENDENCIES
                WHERE (
                    UPPER(REFERENCING_DATABASE) = {_sql_literal(db)}
                    AND UPPER(REFERENCING_SCHEMA) = {_sql_literal(sch)}
                    AND UPPER(REFERENCING_OBJECT_NAME) = {_sql_literal(obj)}
                ) OR (
                    UPPER(REFERENCED_DATABASE) = {_sql_literal(db)}
                    AND UPPER(REFERENCED_SCHEMA) = {_sql_literal(sch)}
                    AND UPPER(REFERENCED_OBJECT_NAME) = {_sql_literal(obj)}
                )
                """
                cur.execute(dep_sql)
                dep_rows = cur.fetchall()
                dep_headers = [d[0] for d in (cur.description or [])]
                _write_csv(bundle_dir / "02_dependency_edges.csv", dep_headers, dep_rows)
            except Exception as exc:
                summary["errors"].append(f"object_dependencies:{exc}")

            access_rows: List[Tuple[Any, ...]] = []
            access_headers: List[str] = []
            try:
                access_sql = f"""
                WITH q AS (
                    SELECT QUERY_ID
                    FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
                    WHERE START_TIME >= DATEADD(DAY, -{lookback}, CURRENT_TIMESTAMP())
                    AND QUERY_TEXT ILIKE {_sql_literal('%' + _to_fqn(db, sch, obj) + '%')}
                    ORDER BY START_TIME DESC
                    LIMIT 500
                )
                SELECT ah.QUERY_ID, ah.BASE_OBJECTS_ACCESSED, ah.DIRECT_OBJECTS_ACCESSED
                FROM SNOWFLAKE.ACCOUNT_USAGE.ACCESS_HISTORY ah
                JOIN q ON q.QUERY_ID = ah.QUERY_ID
                """
                cur.execute(access_sql)
                access_rows = cur.fetchall()
                access_headers = [d[0] for d in (cur.description or [])]
            except Exception as exc:
                summary["errors"].append(f"access_history:{exc}")

            refs_payload: Dict[str, Any] = {"tasks": [], "pipes": [], "procedures": []}
            refs_queries: Dict[str, List[str]] = {
                "tasks": [],
                "pipes": [
                    f"""
                    SELECT PIPE_CATALOG AS DATABASE_NAME, PIPE_SCHEMA AS SCHEMA_NAME, PIPE_NAME AS NAME, DEFINITION
                    FROM SNOWFLAKE.ACCOUNT_USAGE.PIPES
                    WHERE DEFINITION ILIKE {_sql_literal('%' + obj + '%')}
                    ORDER BY CREATED DESC
                    LIMIT 200
                    """,
                    f"""
                    SELECT PIPE_CATALOG AS DATABASE_NAME, PIPE_SCHEMA AS SCHEMA_NAME, PIPE_NAME AS NAME, DEFINITION
                    FROM {db}.INFORMATION_SCHEMA.PIPES
                    WHERE DEFINITION ILIKE {_sql_literal('%' + obj + '%')}
                    LIMIT 200
                    """,
                ],
                "procedures": [
                    f"""
                    SELECT PROCEDURE_CATALOG, PROCEDURE_SCHEMA, PROCEDURE_NAME
                    FROM {db}.INFORMATION_SCHEMA.PROCEDURES
                    WHERE PROCEDURE_DEFINITION ILIKE {_sql_literal('%' + obj + '%')}
                    LIMIT 200
                    """,
                ],
            }
            if include_task_refs:
                refs_queries["tasks"] = [
                    f"""
                    SELECT DATABASE_NAME, SCHEMA_NAME, NAME, QUERY_TEXT
                    FROM SNOWFLAKE.ACCOUNT_USAGE.TASKS
                    WHERE QUERY_TEXT ILIKE {_sql_literal('%' + obj + '%')}
                    ORDER BY CREATED DESC
                    LIMIT 200
                    """,
                    f"""
                    SELECT TASK_CATALOG AS DATABASE_NAME, TASK_SCHEMA AS SCHEMA_NAME, TASK_NAME AS NAME, DEFINITION AS QUERY_TEXT
                    FROM {db}.INFORMATION_SCHEMA.TASKS
                    WHERE DEFINITION ILIKE {_sql_literal('%' + obj + '%')}
                    LIMIT 200
                    """,
                ]
            for key, queries in refs_queries.items():
                if not queries:
                    refs_payload[f"{key}_query_variant"] = -1
                    refs_payload[f"{key}_disabled"] = True
                    continue
                try:
                    cols, rows, used_idx = _run_first_success(cur, queries)
                    refs_payload[key] = [dict(zip(cols, row)) for row in rows]
                    refs_payload[f"{key}_query_variant"] = used_idx
                except Exception as exc:
                    summary["errors"].append(f"{key}:{exc}")
                    summary["detailed_errors"].append(
                        {
                            "category": key,
                            "target": target_table,
                            "error": str(exc),
                        }
                    )
            _write_json(bundle_dir / "03_task_pipe_proc_refs.json", refs_payload)

            dependency_levels_payload: Dict[str, Any] = {}
            try:
                transitive_edges = _fetch_transitive_dependency_edges(
                    cur=cur,
                    seed_db=db,
                    seed_schema=sch,
                    seed_obj=obj,
                    max_depth=max_depth,
                )
                dependency_levels_payload = _compute_dependency_levels(
                    transitive_edges, _to_fqn(db, sch, obj)
                )
                dependency_levels_payload["max_depth_used"] = max_depth
                dependency_levels_payload["edge_count"] = len(transitive_edges)
                _write_json(bundle_dir / "05_dependency_levels.json", dependency_levels_payload)
            except Exception as exc:
                summary["errors"].append(f"dependency_levels:{exc}")
                summary["detailed_errors"].append(
                    {
                        "category": "dependency_levels",
                        "target": target_table,
                        "error": str(exc),
                    }
                )

            object_refs: Set[Tuple[str, str, str, str]] = {(db, sch, obj, "TABLE")}

            for row in dep_rows:
                rec = dict(zip(dep_headers, row))
                for side in ("REFERENCING", "REFERENCED"):
                    dom = str(rec.get(f"{side}_OBJECT_DOMAIN", "")).upper()
                    rdb = str(rec.get(f"{side}_DATABASE", "")).upper()
                    rsch = str(rec.get(f"{side}_SCHEMA", "")).upper()
                    robj = str(rec.get(f"{side}_OBJECT_NAME", "")).upper()
                    if dom and rdb and rsch and robj:
                        object_refs.add((rdb, rsch, robj, dom))

            for row in access_rows:
                rec = dict(zip(access_headers, row))
                for field in ("BASE_OBJECTS_ACCESSED", "DIRECT_OBJECTS_ACCESSED"):
                    object_refs.update(_collect_object_refs(rec.get(field)))

            _write_json(
                bundle_dir / "04_objects_resolved.json",
                [
                    {"database": a, "schema": b, "name": c, "domain": d}
                    for a, b, c, d in sorted(object_refs)
                ],
            )

            ddl_written = 0
            samples_written = 0
            if export_ddls_and_samples:
                for rdb, rsch, robj, dom in sorted(object_refs):
                    dom_norm = dom.upper()
                    if dom_norm not in {"TABLE", "BASE TABLE", "VIEW", "MATERIALIZED VIEW", "DYNAMIC TABLE"}:
                        continue
                    filename = _safe_filename(f"{rdb}.{rsch}.{robj}")
                    fqn = _to_fqn(rdb, rsch, robj)

                    try:
                        preferred = ["TABLE"]
                        if dom_norm in {"VIEW", "MATERIALIZED VIEW"}:
                            preferred = ["VIEW"]
                        elif dom_norm == "DYNAMIC TABLE":
                            preferred = ["DYNAMIC TABLE"]
                        ddl_txt = _get_ddl_with_fallback(cur, fqn, preferred_domains=preferred)
                        (ddls_dir / f"{filename}.sql").write_text(str(ddl_txt), encoding="utf-8")
                        ddl_written += 1
                    except Exception as exc:
                        summary["errors"].append(f"ddl_failed:{dom_norm}:{fqn}")
                        summary["detailed_errors"].append(
                            {
                                "category": "ddl",
                                "target": fqn,
                                "domain": dom_norm,
                                "error": str(exc),
                            }
                        )

                    try:
                        cur.execute(f"SELECT * FROM {_quoted_fqn(rdb, rsch, robj)} LIMIT {limit}")
                        rows = cur.fetchall()
                        headers = [d[0] for d in (cur.description or [])]
                        _write_csv(samples_dir / f"{filename}.csv", headers, rows)
                        samples_written += 1
                    except Exception as exc:
                        # Fallback path for connector conversion issues: cast all columns to VARCHAR.
                        try:
                            cur.execute(f"DESCRIBE TABLE {_quoted_fqn(rdb, rsch, robj)}")
                            cols = [str(r[0]) for r in cur.fetchall() if r and r[0]]
                            if cols:
                                sel = ", ".join([f"TO_VARCHAR({_safe_ident(c)}) AS {_safe_ident(c)}" for c in cols])
                                cur.execute(f"SELECT {sel} FROM {_quoted_fqn(rdb, rsch, robj)} LIMIT {limit}")
                                rows = cur.fetchall()
                                headers = [d[0] for d in (cur.description or [])]
                                _write_csv(samples_dir / f"{filename}.csv", headers, rows)
                                samples_written += 1
                                continue
                        except Exception as exc2:
                            summary["detailed_errors"].append(
                                {
                                    "category": "sample_fallback",
                                    "target": fqn,
                                    "error": str(exc2),
                                }
                            )
                        summary["errors"].append(f"sample_failed:{fqn}")
                        summary["detailed_errors"].append(
                            {
                                "category": "sample",
                                "target": fqn,
                                "error": str(exc),
                            }
                        )

            (bundle_dir / "ANALYSIS.md").write_text(
                "\n".join(
                    [
                        f"# Snowflake Lineage Bundle: `{target_table}`",
                        "",
                        "## Structure",
                        f"- Target object: `{target_table}`",
                        f"- Resolved related objects: `{len(object_refs)}`",
                        f"- DDL files written: `{ddl_written}`",
                        f"- Sample CSV files written: `{samples_written}`",
                        "",
                        "## How It Works (Observed)",
                        "- Uses ACCOUNT_USAGE dependencies where available.",
                        "- Uses query/access history lookback to enrich lineage.",
                        "- Computes transitive dependency levels by depth.",
                        "- Optionally exports DDL and top-N samples for resolved table/view objects.",
                        "",
                    ]
                )
                + "\n",
                encoding="utf-8",
            )

            summary.update(
                {
                    "objects_resolved": len(object_refs),
                    "ddl_files_written": ddl_written,
                    "sample_files_written": samples_written,
                    "dependency_levels_file": str(bundle_dir / "05_dependency_levels.json"),
                    "dependency_edges_transitive": int(dependency_levels_payload.get("edge_count", 0))
                    if dependency_levels_payload
                    else 0,
                    "analysis_file": str(bundle_dir / "ANALYSIS.md"),
                }
            )
            _write_json(bundle_dir / "99_summary.json", summary)
            return json.dumps(summary, indent=2, default=str)
        except Exception as exc:
            summary["errors"].append(str(exc))
            _write_json(bundle_dir / "99_summary.json", summary)
            return json.dumps(summary, indent=2, default=str)
        finally:
            try:
                if conn is not None:
                    conn.close()
            except Exception:
                pass

    return {"tools": ["snowflake_extract_lineage_bundle"]}
