"""
HubSpot pipelines helpers (drift/apply with guardrails).
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from g_gremlin.base import AuthenticationError, IntegrationError, RateLimitError, TransientError
from g_gremlin.hubspot import HubSpotClient
from g_gremlin.hubspot_infra import HubSpotInfraError, _request_with_scope_handling

PIPELINES_BASE_URL = "https://api.hubapi.com/crm/v3/pipelines"


class HubSpotPipelinesError(HubSpotInfraError):
    """Pipelines API error."""


@dataclass
class PipelineStageChange:
    stage_id: str
    change_type: str
    details: Dict[str, Any]


@dataclass
class PipelinePlan:
    object_type: str
    pipeline_id: str
    pipeline_updates: Dict[str, Any] = field(default_factory=dict)
    stage_creates: List[Dict[str, Any]] = field(default_factory=list)
    stage_updates: List[PipelineStageChange] = field(default_factory=list)
    stage_deletes: List[PipelineStageChange] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "object_type": self.object_type,
            "pipeline_id": self.pipeline_id,
            "pipeline_updates": self.pipeline_updates,
            "stage_creates": self.stage_creates,
            "stage_updates": [
                {"stage_id": s.stage_id, "change_type": s.change_type, "details": s.details}
                for s in self.stage_updates
            ],
            "stage_deletes": [
                {"stage_id": s.stage_id, "change_type": s.change_type, "details": s.details}
                for s in self.stage_deletes
            ],
        }


def list_pipelines(client: HubSpotClient, object_type: str) -> List[Dict[str, Any]]:
    url = f"{PIPELINES_BASE_URL}/{object_type}"
    try:
        resp = _request_with_scope_handling(client, "GET", url)
        payload = resp.json()
        return payload.get("results", []) or []
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotPipelinesError(str(exc)) from exc


def get_pipeline(client: HubSpotClient, object_type: str, pipeline_id: str) -> Dict[str, Any]:
    url = f"{PIPELINES_BASE_URL}/{object_type}/{pipeline_id}"
    try:
        resp = _request_with_scope_handling(client, "GET", url)
        return resp.json()
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotPipelinesError(str(exc)) from exc


def export_pipeline_spec(object_type: str, payload: Dict[str, Any]) -> Dict[str, Any]:
    spec = {
        "object_type": object_type,
        "pipeline_id": payload.get("id"),
        "label": payload.get("label"),
        "display_order": payload.get("displayOrder"),
        "stages": [],
    }
    for stage in payload.get("stages", []) or []:
        spec["stages"].append(
            {
                "id": stage.get("id"),
                "label": stage.get("label"),
                "display_order": stage.get("displayOrder"),
                "metadata": stage.get("metadata") or {},
            }
        )
    return spec


def load_pipeline_spec(path: Path) -> Dict[str, Any]:
    if not path.exists():
        raise HubSpotPipelinesError(f"Pipeline spec not found: {path}")
    if path.suffix.lower() != ".json":
        raise HubSpotPipelinesError("Pipeline spec files must be JSON.")
    return json.loads(path.read_text(encoding="utf-8"))


def plan_pipeline_changes(local_spec: Dict[str, Any], remote_payload: Dict[str, Any]) -> PipelinePlan:
    object_type = local_spec.get("object_type") or ""
    pipeline_id = local_spec.get("pipeline_id") or local_spec.get("id") or remote_payload.get("id")
    if not object_type or not pipeline_id:
        raise HubSpotPipelinesError("Pipeline spec missing object_type or pipeline_id.")

    plan = PipelinePlan(object_type=object_type, pipeline_id=str(pipeline_id))
    if local_spec.get("label") != remote_payload.get("label"):
        plan.pipeline_updates["label"] = local_spec.get("label")
    if local_spec.get("display_order") != remote_payload.get("displayOrder"):
        plan.pipeline_updates["displayOrder"] = local_spec.get("display_order")

    remote_stages = remote_payload.get("stages", []) or []
    remote_index = {stage.get("id"): stage for stage in remote_stages if stage.get("id")}

    desired_stages = local_spec.get("stages", []) or []
    desired_index = {stage.get("id"): stage for stage in desired_stages if stage.get("id")}

    for stage in desired_stages:
        stage_id = stage.get("id")
        if stage_id and stage_id in remote_index:
            current = remote_index[stage_id]
            updates = _diff_stage(current, stage)
            if updates:
                plan.stage_updates.append(
                    PipelineStageChange(stage_id=str(stage_id), change_type="update", details=updates)
                )
        else:
            plan.stage_creates.append(stage)

    for stage_id, remote_stage in remote_index.items():
        if stage_id not in desired_index:
            plan.stage_deletes.append(
                PipelineStageChange(stage_id=str(stage_id), change_type="delete", details=remote_stage)
            )

    plan.stage_updates.sort(key=lambda s: s.stage_id)
    plan.stage_deletes.sort(key=lambda s: s.stage_id)
    plan.stage_creates.sort(key=lambda s: s.get("label") or s.get("id") or "")

    return plan


def apply_pipeline_plan(
    client: HubSpotClient,
    plan: PipelinePlan,
    local_spec: Dict[str, Any],
    *,
    allow_delete: bool = False,
) -> Dict[str, Any]:
    object_type = plan.object_type
    pipeline_id = plan.pipeline_id

    summary = {
        "pipeline_updated": False,
        "stages_created": 0,
        "stages_updated": 0,
        "stages_deleted": 0,
    }

    if plan.pipeline_updates:
        url = f"{PIPELINES_BASE_URL}/{object_type}/{pipeline_id}"
        _request_with_scope_handling(client, "PATCH", url, json_body=plan.pipeline_updates)
        summary["pipeline_updated"] = True

    for stage in plan.stage_creates:
        url = f"{PIPELINES_BASE_URL}/{object_type}/{pipeline_id}/stages"
        payload = _build_stage_payload(stage, include_id=False)
        _request_with_scope_handling(client, "POST", url, json_body=payload)
        summary["stages_created"] += 1

    for update in plan.stage_updates:
        url = f"{PIPELINES_BASE_URL}/{object_type}/{pipeline_id}/stages/{update.stage_id}"
        _request_with_scope_handling(client, "PATCH", url, json_body=update.details)
        summary["stages_updated"] += 1

    if plan.stage_deletes and not allow_delete:
        raise HubSpotPipelinesError(
            "Stage deletions requested but --allow-delete not set."
        )

    for deletion in plan.stage_deletes:
        url = f"{PIPELINES_BASE_URL}/{object_type}/{pipeline_id}/stages/{deletion.stage_id}"
        _request_with_scope_handling(client, "DELETE", url)
        summary["stages_deleted"] += 1

    return summary


def backup_pipeline(object_type: str, pipeline_id: str, payload: Dict[str, Any], backup_dir: Path) -> Path:
    backup_dir.mkdir(parents=True, exist_ok=True)
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    path = backup_dir / f"{pipeline_id}-{timestamp}.json"
    path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
    return path


def format_pipeline_plan(plan: PipelinePlan) -> str:
    lines = []
    lines.append(f"Pipeline: {plan.pipeline_id} ({plan.object_type})")
    if plan.pipeline_updates:
        lines.append("Pipeline updates:")
        for key, value in plan.pipeline_updates.items():
            lines.append(f"  ~ {key}: {value}")
    if plan.stage_creates:
        lines.append("Stages to create:")
        for stage in plan.stage_creates:
            lines.append(f"  + {stage.get('label')}")
    if plan.stage_updates:
        lines.append("Stages to update:")
        for stage in plan.stage_updates:
            lines.append(f"  ~ {stage.stage_id}")
    if plan.stage_deletes:
        lines.append("Stages to delete:")
        for stage in plan.stage_deletes:
            lines.append(f"  - {stage.stage_id}")
    if not any([plan.pipeline_updates, plan.stage_creates, plan.stage_updates, plan.stage_deletes]):
        lines.append("No changes.")
    return "\n".join(lines)


def _diff_stage(current: Dict[str, Any], desired: Dict[str, Any]) -> Dict[str, Any]:
    updates: Dict[str, Any] = {}
    if desired.get("label") is not None and desired.get("label") != current.get("label"):
        updates["label"] = desired.get("label")
    if desired.get("display_order") is not None and desired.get("display_order") != current.get("displayOrder"):
        updates["displayOrder"] = desired.get("display_order")
    if "metadata" in desired and desired.get("metadata") != current.get("metadata"):
        updates["metadata"] = desired.get("metadata")
    return updates


def _build_stage_payload(stage: Dict[str, Any], *, include_id: bool) -> Dict[str, Any]:
    payload = {
        "label": stage.get("label"),
        "displayOrder": stage.get("display_order"),
        "metadata": stage.get("metadata") or {},
    }
    if include_id and stage.get("id"):
        payload["id"] = stage.get("id")
    return payload
