"""
DBT metadata source that reads from manifest.json.

This source parses the DBT manifest.json file generated by `dbt compile` or `dbt run`
to extract metadata about models, sources, seeds, and other DBT resources.
"""

import json
from pathlib import Path
from typing import Any, Dict, Optional

import pandas as pd
from datadict_connector_base import MetadataSource

from datadict_connector_dbt.dbt_keys import dbt_key_from_unique_id


class DbtManifestSource(MetadataSource):
    """
    DBT metadata source that parses manifest.json.

    The manifest.json file is generated by DBT and contains complete metadata
    about all resources in the DBT project including models, sources, seeds,
    exposures, tests, and their dependencies.
    """

    def __init__(self):
        self.manifest_path: Optional[Path] = None
        self.manifest_data: Optional[Dict[str, Any]] = None

    def set_credentials(self, credentials: Dict[str, Any]) -> None:
        """
        Set the path to the manifest.json file.

        Args:
            credentials: Dict with 'manifest_path' key pointing to manifest.json
                        Example: {"manifest_path": "/path/to/target/manifest.json"}
        """
        creds = dict(credentials)
        project_root = creds.pop("__project_root", None)
        catalog_root = creds.pop("__catalog_root", None)

        manifest_candidates: list[Path] = []

        # Helper to resolve relative paths against known bases
        def _resolve_path(path_str: str) -> list[Path]:
            path = Path(path_str)
            if path.is_absolute():
                return [path]

            bases: list[Path] = []
            if project_root:
                bases.append(Path(project_root))
            if catalog_root:
                bases.append(Path(catalog_root))
            bases.append(Path.cwd())

            return [(base / path).resolve() for base in bases]

        manifest_value = creds.get("manifest_path")
        if isinstance(manifest_value, str):
            manifest_candidates.extend(_resolve_path(manifest_value))

        target_path_value = creds.get("target_path")
        if not manifest_candidates and isinstance(target_path_value, str):
            for target_candidate in _resolve_path(target_path_value):
                manifest_candidates.append(target_candidate / "manifest.json")

        if not manifest_candidates:
            raise ValueError("credentials must contain 'manifest_path' or 'target_path'")

        resolved_manifest: Optional[Path] = None
        for candidate in manifest_candidates:
            if candidate.exists():
                resolved_manifest = candidate
                break

        if resolved_manifest is None:
            searched = ", ".join(str(path) for path in manifest_candidates)
            raise FileNotFoundError(f"Manifest file not found. Checked: {searched}")

        self.manifest_path = resolved_manifest
        self._load_manifest()

    def _load_manifest(self) -> None:
        """Load and parse the manifest.json file."""
        if not self.manifest_path:
            raise ValueError("manifest_path not set")

        with open(self.manifest_path, "r", encoding="utf-8") as f:
            self.manifest_data = json.load(f)

    def read_metadata(self) -> pd.DataFrame:
        """
        Read metadata from DBT manifest and return as pandas DataFrame.

        Returns DataFrame with columns required by sync system:
        - type: Item type (table for models/seeds, column for columns, doc for exposures)
        - name: Item name
        - key: DBT unique_id (e.g., "model.my_project.customers")
        - sub_type: DBT resource type (model, seed, exposure, etc.)
        - data_type: Data type for columns
        - parent_key: Parent item's unique_id (null for root items)
        """
        if not self.manifest_data:
            raise ValueError("Manifest not loaded. Call set_credentials first.")

        all_items = []

        # Extract models
        all_items.extend(self._extract_models())

        # Extract seeds
        all_items.extend(self._extract_seeds())

        # Extract exposures
        all_items.extend(self._extract_exposures())

        # Create DataFrame
        df = pd.DataFrame(all_items)
        return df

    def read_lineage(self) -> Optional[pd.DataFrame]:
        """Extract lineage edges from manifest dependencies."""
        if not self.manifest_data:
            if not self.manifest_path:
                raise ValueError("Manifest not loaded. Call set_credentials first.")
            self._load_manifest()

        manifest = self.manifest_data or {}
        edges: list[dict[str, Any]] = []

        def _add_edge(upstream_id: str, downstream_id: str, resource_type: str) -> None:
            edges.append(
                {
                    "src_key": dbt_key_from_unique_id(upstream_id),
                    "dst_key": dbt_key_from_unique_id(downstream_id),
                    "edge_type": "depends_on",
                    "properties": {
                        "exact_match": True,
                        "upstream_unique_id": upstream_id,
                        "downstream_unique_id": downstream_id,
                        "downstream_resource_type": resource_type,
                    },
                }
            )

        nodes = manifest.get("nodes", {})
        for unique_id, node in nodes.items():
            resource_type = node.get("resource_type", "node")
            depends_on = node.get("depends_on", {})
            upstream_nodes = depends_on.get("nodes", [])
            for upstream_unique_id in upstream_nodes:
                _add_edge(upstream_unique_id, unique_id, resource_type)

        exposures = manifest.get("exposures", {})
        for unique_id, exposure in exposures.items():
            depends_on = exposure.get("depends_on", {})
            upstream_nodes = depends_on.get("nodes", [])
            for upstream_unique_id in upstream_nodes:
                _add_edge(upstream_unique_id, unique_id, "exposure")

        if not edges:
            return None

        return pd.DataFrame(edges)

    def _extract_models(self) -> list:
        """Extract model metadata from manifest."""
        items = []
        nodes = self.manifest_data.get("nodes", {})

        for unique_id, node in nodes.items():
            # Only process models
            if node.get("resource_type") != "model":
                continue

            model_name = node.get("name", "")
            node.get("package_name", "")
            description = node.get("description", "")

            # Create model item
            items.append(
                {
                    "type": "table",
                    "name": model_name,
                    "key": unique_id,
                    "sub_type": "model",
                    "data_type": None,
                    "parent_key": None,
                    "description": description,
                }
            )

            # Extract columns for this model
            columns = node.get("columns", {})
            for col_name, col_data in columns.items():
                column_key = f"{unique_id}.{col_name}"
                items.append(
                    {
                        "type": "column",
                        "name": col_name,
                        "key": column_key,
                        "sub_type": "column",
                        "data_type": col_data.get("data_type"),
                        "parent_key": unique_id,
                        "description": col_data.get("description", ""),
                    }
                )

        return items

    def _extract_seeds(self) -> list:
        """Extract seed metadata from manifest."""
        items = []
        nodes = self.manifest_data.get("nodes", {})

        for unique_id, node in nodes.items():
            # Only process seeds
            if node.get("resource_type") != "seed":
                continue

            seed_name = node.get("name", "")
            description = node.get("description", "")

            # Create seed item
            items.append(
                {
                    "type": "table",
                    "name": seed_name,
                    "key": unique_id,
                    "sub_type": "seed",
                    "data_type": None,
                    "parent_key": None,
                    "description": description,
                }
            )

            # Extract columns for this seed
            columns = node.get("columns", {})
            for col_name, col_data in columns.items():
                column_key = f"{unique_id}.{col_name}"
                items.append(
                    {
                        "type": "column",
                        "name": col_name,
                        "key": column_key,
                        "sub_type": "column",
                        "data_type": col_data.get("data_type"),
                        "parent_key": unique_id,
                        "description": col_data.get("description", ""),
                    }
                )

        return items

    def _extract_exposures(self) -> list:
        """Extract exposure metadata from manifest."""
        items = []
        exposures = self.manifest_data.get("exposures", {})

        for unique_id, exposure in exposures.items():
            exposure_name = exposure.get("name", "")
            exposure_type = exposure.get("type", "dashboard")
            description = exposure.get("description", "")
            url = exposure.get("url")
            owner = exposure.get("owner", {})

            # Create exposure item
            items.append(
                {
                    "type": "doc",
                    "name": exposure_name,
                    "key": unique_id,
                    "sub_type": "exposure",
                    "data_type": None,
                    "parent_key": None,
                    "description": description,
                    "exposure_type": exposure_type,
                    "url": url,
                    "owner": owner.get("name") if isinstance(owner, dict) else owner,
                }
            )

        return items

    def close(self) -> None:
        """
        Close the source and release resources.

        For manifest-based source, this just clears the loaded data.
        """
        self.manifest_data = None
