"""
HubSpot schema cache utilities.
"""

from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Any, Dict, List

from g_gremlin import __version__
from g_gremlin.hubspot import HubSpotClient, HubSpotError
from g_gremlin.paths import gremlin_path

SCHEMA_VERSION = 1
DEFAULT_CACHE_DIR = gremlin_path("cache", "hubspot")


@dataclass
class ObjectSchema:
    name: str
    label: str
    is_custom: bool
    primary_display: str
    searchable_properties: List[str]

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "label": self.label,
            "is_custom": self.is_custom,
            "primary_display": self.primary_display,
            "searchable_properties": self.searchable_properties,
        }


@dataclass
class PropertySchema:
    name: str
    label: str
    type: str
    field_type: str
    group_name: str
    is_custom: bool

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "label": self.label,
            "type": self.type,
            "field_type": self.field_type,
            "group_name": self.group_name,
            "is_custom": self.is_custom,
        }


@dataclass
class AssociationSchema:
    from_type: str
    to_type: str
    label: str
    type_id: int

    def to_dict(self) -> Dict[str, Any]:
        return {
            "from_type": self.from_type,
            "to_type": self.to_type,
            "label": self.label,
            "type_id": self.type_id,
        }


@dataclass
class SchemaSyncResult:
    portal_id: str
    standard_objects: int
    custom_objects: int
    properties_count: int
    associations_count: int
    cache_path: Path
    synced_at: str

    def to_dict(self) -> Dict[str, Any]:
        return {
            "portal_id": self.portal_id,
            "standard_objects": self.standard_objects,
            "custom_objects": self.custom_objects,
            "properties_count": self.properties_count,
            "associations_count": self.associations_count,
            "cache_path": str(self.cache_path),
            "synced_at": self.synced_at,
        }


class HubSpotSchemaCache:
    def __init__(self, portal_id: str, cache_dir: Path = DEFAULT_CACHE_DIR) -> None:
        self.portal_id = str(portal_id)
        self.cache_dir = cache_dir / self.portal_id
        self.cache_path = self.cache_dir / "schema.json"

    def sync(self, client: HubSpotClient) -> SchemaSyncResult:
        schemas = client.get_schemas()
        objects: List[ObjectSchema] = []
        properties: Dict[str, List[PropertySchema]] = {}
        associations: List[AssociationSchema] = []

        for schema in schemas:
            name = schema.get("name") or schema.get("objectTypeId") or ""
            labels = schema.get("labels") or {}
            label = labels.get("plural") or labels.get("singular") or name
            is_custom = bool(schema.get("isCustom"))
            if not schema.get("isCustom") and name and name.startswith("p"):
                is_custom = True
            primary_display = schema.get("primaryDisplayProperty") or ""
            searchable = schema.get("searchableProperties") or []
            objects.append(
                ObjectSchema(
                    name=name,
                    label=label,
                    is_custom=is_custom,
                    primary_display=primary_display,
                    searchable_properties=list(searchable),
                )
            )

        for obj in objects:
            try:
                props = client.get_properties(obj.name)
            except HubSpotError:
                props = []
            properties[obj.name] = [
                PropertySchema(
                    name=prop.get("name") or "",
                    label=prop.get("label") or prop.get("name") or "",
                    type=prop.get("type") or "",
                    field_type=prop.get("fieldType") or "",
                    group_name=prop.get("groupName") or "",
                    is_custom=bool(prop.get("isCustom")),
                )
                for prop in props
            ]

        object_names = [obj.name for obj in objects if obj.name]
        for from_type in object_names:
            for to_type in object_names:
                try:
                    labels = client.get_association_labels(from_type, to_type)
                except HubSpotError:
                    labels = []
                for label in labels:
                    associations.append(
                        AssociationSchema(
                            from_type=from_type,
                            to_type=to_type,
                            label=label.get("label") or "",
                            type_id=int(label.get("typeId") or 0),
                        )
                    )

        synced_at = datetime.now(timezone.utc).isoformat()
        payload = {
            "schema_version": SCHEMA_VERSION,
            "client_version": __version__,
            "synced_at": synced_at,
            "portal_id": self.portal_id,
            "objects": [obj.to_dict() for obj in objects],
            "properties": {k: [p.to_dict() for p in v] for k, v in properties.items()},
            "associations": [assoc.to_dict() for assoc in associations],
        }

        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self.cache_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")

        standard_objects = len([obj for obj in objects if not obj.is_custom])
        custom_objects = len([obj for obj in objects if obj.is_custom])
        properties_count = sum(len(v) for v in properties.values())

        return SchemaSyncResult(
            portal_id=self.portal_id,
            standard_objects=standard_objects,
            custom_objects=custom_objects,
            properties_count=properties_count,
            associations_count=len(associations),
            cache_path=self.cache_path,
            synced_at=synced_at,
        )

    def _read_cache(self) -> Dict[str, Any]:
        if not self.cache_path.exists():
            raise HubSpotError("Schema cache not found. Run g-gremlin hubspot schema sync first.")
        try:
            return json.loads(self.cache_path.read_text(encoding="utf-8"))
        except Exception as exc:
            raise HubSpotError(f"Invalid schema cache: {exc}") from exc

    def get_objects(self) -> List[ObjectSchema]:
        payload = self._read_cache()
        objects = payload.get("objects") or []
        return [
            ObjectSchema(
                name=obj.get("name") or "",
                label=obj.get("label") or "",
                is_custom=bool(obj.get("is_custom")),
                primary_display=obj.get("primary_display") or "",
                searchable_properties=obj.get("searchable_properties") or [],
            )
            for obj in objects
        ]

    def get_properties(self, object_type: str) -> List[PropertySchema]:
        payload = self._read_cache()
        props = (payload.get("properties") or {}).get(object_type, [])
        return [
            PropertySchema(
                name=prop.get("name") or "",
                label=prop.get("label") or "",
                type=prop.get("type") or "",
                field_type=prop.get("field_type") or "",
                group_name=prop.get("group_name") or "",
                is_custom=bool(prop.get("is_custom")),
            )
            for prop in props
        ]

    def get_associations(self, object_type: str) -> List[AssociationSchema]:
        payload = self._read_cache()
        assocs = payload.get("associations") or []
        return [
            AssociationSchema(
                from_type=assoc.get("from_type") or "",
                to_type=assoc.get("to_type") or "",
                label=assoc.get("label") or "",
                type_id=int(assoc.get("type_id") or 0),
            )
            for assoc in assocs
            if assoc.get("from_type") == object_type or assoc.get("to_type") == object_type
        ]

    def is_stale(self, max_age_hours: int = 24) -> bool:
        if not self.cache_path.exists():
            return True
        try:
            payload = json.loads(self.cache_path.read_text(encoding="utf-8"))
            synced_at = payload.get("synced_at")
            if not synced_at:
                return True
            ts = datetime.fromisoformat(synced_at)
            if ts.tzinfo is None:
                ts = ts.replace(tzinfo=timezone.utc)
            return datetime.now(timezone.utc) - ts > timedelta(hours=max_age_hours)
        except Exception:
            return True

    def clear(self) -> None:
        if self.cache_path.exists():
            self.cache_path.unlink()
