"""
Seed database from YAML: contracts, schemas, coercion/validation/metadata artifacts, and reference data.

Used by `pycharter db seed`; supports PostgreSQL (and SQLite) plus MongoDB.
"""

from __future__ import annotations

import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable

try:
    import yaml  # type: ignore[import-untyped]

    YAML_AVAILABLE = True
except ImportError:
    YAML_AVAILABLE = False

from pycharter.db.config import get_db_url, require_alembic
from pycharter.db.metadata_normalizer import (
    LINEAGE_KEYS,
    OWNERSHIP_KEYS,
    ensure_canonical,
    relationship_view,
)
from pycharter.db.models import (
    APIEndpointModel,
    CoercionRuleModel,
    ComplianceFrameworkModel,
    DataContractModel,
    DataFeedModel,
    DomainModel,
    EnvironmentModel,
    MetadataRecordBusinessOwner,
    MetadataRecordBUSME,
    MetadataRecordDomain,
    MetadataRecordEnvironment,
    MetadataRecordITApplicationOwner,
    MetadataRecordITSME,
    MetadataRecordModel,
    MetadataRecordStorageLocation,
    MetadataRecordSupportLead,
    MetadataRecordSystemPull,
    MetadataRecordSystemPush,
    MetadataRecordSystemSource,
    OwnerModel,
    SchemaModel,
    StorageLocationModel,
    SystemModel,
    TagModel,
    ValidationRuleModel,
)
from pycharter.db.models.base import get_session


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def _set_sqlite_timestamps(session: Any, instance: Any) -> None:
    """Set created_at/updated_at on instance when using SQLite (no now() function)."""
    try:
        bind = session.get_bind()
        dialect = getattr(bind, "dialect", None)
        if dialect is None or dialect.name != "sqlite":
            return
    except Exception:
        return
    now = _utc_now()
    if hasattr(instance, "created_at"):
        instance.created_at = now
    if hasattr(instance, "updated_at"):
        instance.updated_at = now


def seed_model(
    session: Any,
    model_class: type,
    seed_file: Path,
    model_name: str,
    id_field: str = "id",
    id_fields: list[str] | None = None,
    transform_fn: Callable[[dict], dict | None] | None = None,
) -> int:
    """Load a YAML file and upsert rows into the given model. Returns count of rows processed."""
    if not seed_file.exists():
        print(f"⚠ No {seed_file.name} file found, skipping {model_name}")
        return 0
    print(f"Loading {model_name}...")
    with open(seed_file, "r") as f:
        data = yaml.safe_load(f) or []
    count = 0
    for item_data in data:
        if transform_fn:
            item_data = transform_fn(item_data)
            if item_data is None:
                continue
        keys = id_fields if id_fields is not None else [id_field]
        item_id = (
            item_data.get(id_field)
            if len(keys) == 1
            else tuple(item_data.get(k) for k in keys)
        )
        if item_id is None or (isinstance(item_id, tuple) and None in item_id):
            print(
                f"⚠ Warning: Skipping {model_name} entry missing key field(s): {keys}"
            )
            continue
        filter_kw = {k: item_data[k] for k in keys if k in item_data}
        existing = session.query(model_class).filter_by(**filter_kw).first()
        if existing:
            for key, value in item_data.items():
                if hasattr(existing, key):
                    setattr(existing, key, value)
            _set_sqlite_timestamps(session, existing)
            print(f"  Updated {model_name}: {item_id}")
        else:
            item = model_class(**item_data)
            _set_sqlite_timestamps(session, item)
            session.add(item)
            print(f"  Created {model_name}: {item_id}")
        count += 1
    session.commit()
    print(f"✓ Loaded {count} {model_name}(s)")
    return count


def _seed_contracts_only(
    seed_path: Path, session: Any
) -> dict[tuple[str, str], int | None]:
    """Seed data_contracts from contracts.yaml. Returns (name, version) -> data_contract_id."""
    contracts_file = seed_path / "contracts.yaml"
    if not contracts_file.exists():
        print("⚠ No contracts.yaml found, skipping contract artifacts")
        return {}
    with open(contracts_file, "r") as f:
        contracts = yaml.safe_load(f) or []
    if not contracts:
        return {}
    print("Loading contracts...")
    lookup: dict[tuple[str, str], int | None] = {}
    for entry in contracts:
        name = entry.get("name")
        version = entry.get("version")
        if not name or not version:
            print("⚠ Skipping contract entry missing name or version")
            continue
        existing = (
            session.query(DataContractModel)
            .filter(
                DataContractModel.name == name,
                DataContractModel.version == version,
            )
            .first()
        )
        if existing:
            print(f"  Skipped (exists): {name}@{version}")
            lookup[(name, version)] = existing.id
            continue
        contract = DataContractModel(
            name=name,
            version=version,
            status=entry.get("status") or "active",
            description=entry.get("description"),
        )
        _set_sqlite_timestamps(session, contract)
        session.add(contract)
        session.flush()
        lookup[(name, version)] = contract.id
        print(f"  Created contract: {name}@{version}")
    session.commit()
    if lookup:
        print(f"✓ Loaded {len(lookup)} contract(s)")
    return lookup


def _transform_schema(
    record: dict, contract_lookup: dict[tuple[str, str], int | None]
) -> dict | None:
    """Resolve contract ref and build SchemaModel kwargs with schema_data."""
    name = record.get("contract_name")
    version = record.get("contract_version")
    if not name or not version:
        return None
    key = (name, version)
    if key not in contract_lookup:
        return None
    data_contract_id = contract_lookup[key]
    title = record.get("title") or f"{name}_schema"
    version_val = record.get("version") or version
    schema_data = {
        k: v
        for k, v in record.items()
        if k not in ("contract_name", "contract_version")
    }
    schema_data.setdefault("title", title)
    schema_data.setdefault("version", version_val)
    return {
        "title": title,
        "data_contract_id": data_contract_id,
        "version": version_val,
        "schema_data": schema_data,
    }


def _build_contract_artifact_lookup(
    session: Any, contract_lookup: dict[tuple[str, str], int | None]
) -> dict[tuple[str, str], tuple[int, int | None]]:
    """Build (name, version) -> (data_contract_id, schema_id) after schemas are seeded."""
    full: dict[tuple[str, str], tuple[int, int | None]] = {}
    for (name, version), data_contract_id in contract_lookup.items():
        schema_row = (
            session.query(SchemaModel)
            .filter_by(data_contract_id=data_contract_id)
            .first()
        )
        schema_id = schema_row.id if schema_row else None
        full[(name, version)] = (data_contract_id, schema_id)
    return full


def _transform_artifact(
    record: dict,
    contract_lookup: dict[tuple[str, str], tuple[int, int | None]],
    include_schema_id: bool = False,
) -> dict | None:
    """Resolve contract ref to data_contract_id (and schema_id). Drop ref keys; return None if not found."""
    name = record.get("contract_name")
    version = record.get("contract_version")
    if not name or not version:
        return None
    key = (name, version)
    if key not in contract_lookup:
        return None
    data_contract_id, schema_id = contract_lookup[key]
    out = {
        k: v
        for k, v in record.items()
        if k not in ("contract_name", "contract_version")
    }
    out["data_contract_id"] = data_contract_id
    if include_schema_id:
        out["schema_id"] = schema_id
    return out


# Column names on MetadataRecordModel that we can set from seed (excludes id, relationships)
_METADATA_RECORD_COLUMNS = {
    "title",
    "data_contract_id",
    "version",
    "status",
    "type",
    "description",
    "created_at",
    "updated_at",
    "created_by",
    "updated_by",
    "governance_rules",
    "payload",
}


def _to_json_serializable(obj: Any) -> Any:
    """Recursively convert UUID/datetime to JSON-serializable types for JSON columns."""
    if obj is None:
        return None
    if isinstance(obj, uuid.UUID):
        return str(obj)
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, dict):
        return {k: _to_json_serializable(v) for k, v in obj.items()}
    if isinstance(obj, (list, tuple)):
        return [_to_json_serializable(v) for v in obj]
    return obj


def _transform_metadata_record(
    record: dict,
    contract_lookup: dict[tuple[str, str], tuple[int, int | None]],
) -> dict | None:
    """Transform metadata seed entry: resolve contract, normalize to canonical, store payload."""
    base = _transform_artifact(record, contract_lookup, include_schema_id=False)
    if not base:
        return None
    canonical = ensure_canonical(base)
    # Only pass keys that are columns on MetadataRecordModel
    row = {k: v for k, v in canonical.items() if k in _METADATA_RECORD_COLUMNS}
    row["data_contract_id"] = base["data_contract_id"]
    # JSON columns must not contain UUID/datetime objects; convert for serialization
    row["payload"] = _to_json_serializable(canonical)
    if "governance_rules" in row and row["governance_rules"] is not None:
        row["governance_rules"] = _to_json_serializable(row["governance_rules"])
    if "title" not in row and canonical.get("title"):
        row["title"] = canonical["title"]
    if "version" not in row and canonical.get("version"):
        row["version"] = canonical["version"]
    return row


def _seed_metadata_records(
    seed_path: Path,
    session: Any,
    full_lookup: dict[tuple[str, str], tuple[int, int | None]],
    id_fields: list[str],
) -> int:
    """Seed metadata_records from metadata.yaml with canonical payload and relationship sync."""
    metadata_file = seed_path / "metadata.yaml"
    if not metadata_file.exists():
        print("⚠ No metadata.yaml found, skipping metadata_records")
        return 0
    with open(metadata_file, "r") as f:
        data = yaml.safe_load(f) or []
    count = 0
    for record in data:
        row = _transform_metadata_record(record, full_lookup)
        if row is None:
            continue
        filter_kw = {k: row[k] for k in id_fields if k in row}
        if len(filter_kw) != len(id_fields):
            print(
                f"⚠ Warning: Skipping metadata entry missing key field(s): {id_fields}"
            )
            continue
        existing = session.query(MetadataRecordModel).filter_by(**filter_kw).first()
        if existing:
            for key, value in row.items():
                if hasattr(existing, key):
                    setattr(existing, key, value)
            _set_sqlite_timestamps(session, existing)
            session.flush()
            _sync_metadata_relationships_from_payload(session, existing)
            count += 1
        else:
            item = MetadataRecordModel(
                **{k: v for k, v in row.items() if k in _METADATA_RECORD_COLUMNS}
            )
            _set_sqlite_timestamps(session, item)
            session.add(item)
            session.flush()
            _sync_metadata_relationships_from_payload(session, item)
            count += 1
    session.commit()
    print(f"✓ Loaded {count} metadata record(s)")
    return count


_OWNERSHIP_MODELS = (
    MetadataRecordBusinessOwner,
    MetadataRecordBUSME,
    MetadataRecordITApplicationOwner,
    MetadataRecordITSME,
    MetadataRecordSupportLead,
)
_LINEAGE_MODELS = (
    MetadataRecordSystemPull,
    MetadataRecordSystemPush,
    MetadataRecordSystemSource,
)


def _sync_metadata_relationships_from_payload(
    session: Any, meta: MetadataRecordModel
) -> None:
    """Sync join tables from payload; clears existing first."""
    payload = meta.payload
    if not payload or not isinstance(payload, dict):
        return
    view = relationship_view(payload)
    mid = meta.id
    if mid is None:
        return
    for model_class in (
        *_OWNERSHIP_MODELS,
        *_LINEAGE_MODELS,
        MetadataRecordDomain,
        MetadataRecordStorageLocation,
        MetadataRecordEnvironment,
    ):
        session.query(model_class).filter_by(metadata_record_id=mid).delete()
    for rel_type, model_class in zip(OWNERSHIP_KEYS, _OWNERSHIP_MODELS):
        for owner_id in view.get(rel_type) or []:
            if isinstance(owner_id, str):
                link = model_class(
                    id=uuid.uuid4(), metadata_record_id=mid, owner_id=owner_id
                )
                _set_sqlite_timestamps(session, link)
                session.add(link)
    # Avoid autoflush when querying so link inserts are not flushed before we finish adding them
    with session.no_autoflush:
        system_by_name = {s.name: s.id for s in session.query(SystemModel).all()}
    for rel_type, model_class in zip(LINEAGE_KEYS, _LINEAGE_MODELS):
        for name in view.get(rel_type) or []:
            if isinstance(name, str) and name in system_by_name:
                link = model_class(
                    id=uuid.uuid4(),
                    metadata_record_id=mid,
                    system_id=system_by_name[name],
                )
                _set_sqlite_timestamps(session, link)
                session.add(link)
    domain_name = view.get("domain")
    if isinstance(domain_name, str):
        with session.no_autoflush:
            domain = session.query(DomainModel).filter_by(name=domain_name).first()
        if domain:
            link = MetadataRecordDomain(
                id=uuid.uuid4(), metadata_record_id=mid, domain_id=domain.id
            )
            _set_sqlite_timestamps(session, link)
            session.add(link)
    # storage_locations: list of names (str) or list of {name, usage_type}
    with session.no_autoflush:
        storage_by_name = {
            s.name: s.id for s in session.query(StorageLocationModel).all()
        }
    for entry in view.get("storage_locations") or []:
        name = entry.get("name", entry) if isinstance(entry, dict) else entry
        usage_type = entry.get("usage_type") if isinstance(entry, dict) else None
        if isinstance(name, str) and name in storage_by_name:
            link = MetadataRecordStorageLocation(
                id=uuid.uuid4(),
                metadata_record_id=mid,
                storage_location_id=storage_by_name[name],
                usage_type=usage_type,
            )
            _set_sqlite_timestamps(session, link)
            session.add(link)
    # environments: list of environment names
    with session.no_autoflush:
        env_by_name = {e.name: e.id for e in session.query(EnvironmentModel).all()}
    for env_name in view.get("environments") or []:
        if isinstance(env_name, str) and env_name in env_by_name:
            link = MetadataRecordEnvironment(
                id=uuid.uuid4(),
                metadata_record_id=mid,
                environment_id=env_by_name[env_name],
            )
            _set_sqlite_timestamps(session, link)
            session.add(link)
    session.flush()


def seed_contract_artifacts(seed_path: Path, session: Any) -> int:
    """
    Seed contracts → schemas → link schema_id on contracts → coercion/validation/metadata → link FKs on contracts.
    Returns number of contracts seeded.
    """
    contract_lookup = _seed_contracts_only(seed_path, session)
    if not contract_lookup:
        return 0
    id_fields = ["data_contract_id", "title"]
    seed_model(
        session,
        SchemaModel,
        seed_path / "schemas.yaml",
        "schemas",
        id_field="title",
        id_fields=id_fields,
        transform_fn=lambda r: _transform_schema(r, contract_lookup),
    )
    for (name, version), data_contract_id in contract_lookup.items():
        contract = (
            session.query(DataContractModel)
            .filter_by(name=name, version=version)
            .first()
        )
        if not contract or contract.schema_id:
            continue
        schema_row = (
            session.query(SchemaModel)
            .filter_by(data_contract_id=data_contract_id)
            .first()
        )
        if schema_row:
            contract.schema_id = schema_row.id
    session.commit()
    full_lookup = _build_contract_artifact_lookup(session, contract_lookup)
    seed_model(
        session,
        CoercionRuleModel,
        seed_path / "coercion_rules.yaml",
        "coercion_rules",
        id_field="title",
        id_fields=id_fields,
        transform_fn=lambda r: _transform_artifact(
            r, full_lookup, include_schema_id=True
        ),
    )
    seed_model(
        session,
        ValidationRuleModel,
        seed_path / "validation_rules.yaml",
        "validation_rules",
        id_field="title",
        id_fields=id_fields,
        transform_fn=lambda r: _transform_artifact(
            r, full_lookup, include_schema_id=True
        ),
    )
    # Metadata: normalize to canonical, store payload, then sync relationships
    _seed_metadata_records(seed_path, session, full_lookup, id_fields)
    for (name, version), (data_contract_id, _schema_id) in full_lookup.items():
        contract = (
            session.query(DataContractModel)
            .filter_by(name=name, version=version)
            .first()
        )
        if not contract or contract.coercion_rules_id:
            continue
        cr = (
            session.query(CoercionRuleModel)
            .filter_by(data_contract_id=data_contract_id)
            .first()
        )
        vr = (
            session.query(ValidationRuleModel)
            .filter_by(data_contract_id=data_contract_id)
            .first()
        )
        meta = (
            session.query(MetadataRecordModel)
            .filter_by(data_contract_id=data_contract_id)
            .first()
        )
        if cr:
            contract.coercion_rules_id = cr.id
        if vr:
            contract.validation_rules_id = vr.id
        if meta:
            contract.metadata_record_id = meta.id
    session.commit()
    return len(contract_lookup)


def _seed_concepts(seed_path: Path, session: Any) -> int:
    """
    Seed concept vocabulary from ontologies.yaml (or ontologies.jsonld) into
    the wiki concepts table.  Returns the number of concepts seeded.
    """
    for name in ("ontologies.yaml", "ontologies.jsonld"):
        candidate = seed_path / name
        if candidate.exists():
            break
    else:
        return 0

    try:
        from pycharter.wiki.rdf.vocabulary import ingest_vocabulary, load_vocabulary
    except Exception:
        print("⚠ Skipping concept seed (vocabulary loader not available)")
        return 0

    print("Loading concepts...")
    try:
        scheme = load_vocabulary(str(candidate))
        result = ingest_vocabulary(scheme, session, author="seed")
        total = result["total"]
        print(
            f"✓ Loaded {total} concept(s) "
            f"({result['created']} created, {result['updated']} updated)"
        )
        return total
    except Exception as e:
        print(f"  ⚠ Failed to seed concepts: {e}")
        return 0


def _seed_ontologies(seed_path: Path, db_url: str) -> int:
    """
    Seed ontology records from ontology_mappings.yaml into contract_versions (Wiki).
    Only runs for PostgreSQL; requires wiki tables (contract_versions) to exist.
    Returns the number of ontologies seeded.
    """
    if not db_url or "postgres" not in db_url.lower():
        return 0
    mappings_file = seed_path / "ontology_mappings.yaml"
    if not mappings_file.exists():
        return 0
    try:
        from pycharter.metadata_store.postgres import PostgresMetadataStore
    except ImportError:
        print("⚠ Skipping ontology seed (metadata_store not available)")
        return 0
    with open(mappings_file, "r") as f:
        data = yaml.safe_load(f) or []
    if not data:
        return 0
    print("Loading ontologies...")
    client = PostgresMetadataStore(db_url)
    count = 0
    try:
        client.connect()
        for record in data:
            contract_name = record.get("contract_name")
            contract_version = record.get("contract_version")
            if not contract_name or not contract_version:
                print(
                    "  ⚠ Skipping ontology entry missing contract_name or contract_version"
                )
                continue
            ontology = {
                k: v
                for k, v in record.items()
                if k not in ("contract_name", "contract_version")
            }
            if not ontology:
                continue
            try:
                client.store_ontology(contract_name, contract_version, ontology)
                count += 1
                print(f"  Stored ontology: {contract_name}@{contract_version}")
            except Exception as e:
                print(
                    f"  ⚠ Failed to store ontology {contract_name}@{contract_version}: {e}"
                )
        if count:
            print(f"✓ Loaded {count} ontology/ontologies")
    finally:
        client.disconnect()
    return count


def seed_postgresql(seed_path: Path, db_url: str) -> int:
    """Seed PostgreSQL (or SQLite) with reference data and contract artifacts. Returns 0 on success."""
    require_alembic()
    session = get_session(db_url)
    try:
        seed_model(session, OwnerModel, seed_path / "owners.yaml", "owners")
        seed_model(session, DomainModel, seed_path / "domains.yaml", "domains", "name")
        seed_model(session, SystemModel, seed_path / "systems.yaml", "systems", "name")
        seed_model(
            session,
            EnvironmentModel,
            seed_path / "environments.yaml",
            "environments",
            "name",
        )
        seed_model(
            session, DataFeedModel, seed_path / "data_feeds.yaml", "data_feeds", "name"
        )
        seed_model(
            session,
            ComplianceFrameworkModel,
            seed_path / "compliance_frameworks.yaml",
            "compliance_frameworks",
            "name",
        )
        seed_model(session, TagModel, seed_path / "tags.yaml", "tags", "name")
        seed_contract_artifacts(seed_path, session)
        _seed_concepts(seed_path, session)
        _seed_ontologies(seed_path, db_url)
        print("\n✓ Seed data loaded successfully!")
        return 0
    except Exception as e:
        session.rollback()
        raise e
    finally:
        session.close()


def seed_mongodb(seed_path: Path, db_url: str) -> int:
    """Seed MongoDB with owners, domains, systems from YAML. Returns 0 on success, 1 on error."""
    try:
        from datetime import datetime

        from bson import ObjectId
        from pymongo import MongoClient

        database_name = "pycharter"
        if "/" in db_url.rsplit("@", 1)[-1]:
            database_name = db_url.rsplit("/", 1)[-1].split("?")[0]
        client = MongoClient(db_url)
        db = client[database_name]
        try:
            for collection_name, model_name, id_field in [
                ("owners", "owners", "id"),
                ("domains", "domains", "name"),
                ("systems", "systems", "name"),
            ]:
                seed_file = seed_path / f"{collection_name}.yaml"
                if not seed_file.exists():
                    print(f"⚠ No {seed_file.name} file found, skipping {model_name}")
                    continue
                print(f"Loading {model_name}...")
                with open(seed_file, "r") as f:
                    data = yaml.safe_load(f) or []
                collection = db[collection_name]
                count_created = count_updated = 0
                now = datetime.utcnow()
                for item_data in data:
                    item_id = item_data.get(id_field)
                    if not item_id:
                        print(
                            f"⚠ Warning: Skipping {model_name} entry without '{id_field}' field"
                        )
                        continue
                    existing = collection.find_one({id_field: item_id})
                    doc = dict(item_data)
                    if existing:
                        doc["updated_at"] = now
                        doc["created_at"] = existing.get("created_at", now)
                        collection.update_one({id_field: item_id}, {"$set": doc})
                        count_updated += 1
                    else:
                        if collection_name != "owners":
                            doc["_id"] = ObjectId()
                        doc["created_at"] = doc["updated_at"] = now
                        collection.insert_one(doc)
                        count_created += 1
                print(
                    f"✓ Loaded {len(data)} {model_name}(s) ({count_created} created, {count_updated} updated)"
                )
            print("\n✓ Seed data loaded successfully!")
            return 0
        finally:
            client.close()
    except ImportError:
        print("❌ Error: pymongo is required. Install with: pip install pymongo")
        return 1
    except Exception as e:
        print(f"❌ Error seeding MongoDB: {e}")
        import traceback

        traceback.print_exc()
        return 1


def cmd_seed(seed_dir: str | None = None, database_url: str | None = None) -> int:
    """CLI entry: resolve seed dir and DB URL, then run PostgreSQL or MongoDB seed. Returns exit code."""
    if not YAML_AVAILABLE:
        print("❌ Error: PyYAML is required. Install with: pip install pyyaml")
        return 1
    try:
        if seed_dir and any(
            seed_dir.startswith(p)
            for p in (
                "postgresql://",
                "postgres://",
                "mysql://",
                "sqlite://",
                "mongodb://",
                "mongodb+srv://",
            )
        ):
            database_url, seed_dir = seed_dir, None
        db_url = get_db_url(database_url)
        if not db_url:
            return 1
        if seed_dir:
            seed_path = Path(seed_dir)
        else:
            pkg_root = Path(__file__).resolve().parent.parent
            seed_path = pkg_root / "data" / "seed"
        if not seed_path.exists():
            print(f"❌ Error: Seed directory not found: {seed_path}")
            return 1
        print(f"Loading seed data from: {seed_path}")
        if db_url.startswith(("mongodb://", "mongodb+srv://")):
            return seed_mongodb(seed_path, db_url)
        seed_postgresql(seed_path, db_url)
        return 0
    except Exception as e:
        print(f"❌ Error seeding database: {e}")
        import traceback

        traceback.print_exc()
        return 1
