"""
HubSpot Automation (v4) workflow helpers with beta guardrails.
"""

from __future__ import annotations

import json
import logging
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, 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

logger = logging.getLogger(__name__)

AUTOMATION_BETA_ENV = "G_GREMLIN_HUBSPOT_AUTOMATION_BETA"
AUTOMATION_BASE_URL = "https://api.hubapi.com/automation/v4/flows"


class AutomationBetaDisabledError(HubSpotInfraError):
    """Raised when automation beta flag is not enabled."""


class FlowNotFoundError(HubSpotInfraError):
    """Raised when a workflow ID is not found (404)."""


class WorkflowApplyError(HubSpotInfraError):
    """Raised when workflow apply fails."""


@dataclass
class WorkflowDiffResult:
    flow_id: str
    diff: str

    def to_dict(self) -> Dict[str, Any]:
        return {"flow_id": self.flow_id, "diff": self.diff}


def require_automation_beta() -> None:
    if os.getenv(AUTOMATION_BETA_ENV) != "1":
        raise AutomationBetaDisabledError(
            f"Automation v4 is beta. Set {AUTOMATION_BETA_ENV}=1 to enable."
        )


def list_flows(client: HubSpotClient, limit: Optional[int] = None) -> List[Dict[str, Any]]:
    require_automation_beta()
    params = {"limit": min(limit or 100, 100)}
    flows: List[Dict[str, Any]] = []
    for item in _paginate(client, AUTOMATION_BASE_URL, params):
        flows.append(item)
        if limit and len(flows) >= limit:
            break
    return flows


def get_flow(client: HubSpotClient, flow_id: str) -> Dict[str, Any]:
    require_automation_beta()
    url = f"{AUTOMATION_BASE_URL}/{flow_id}"
    try:
        resp = _request_with_scope_handling(client, "GET", url)
        return resp.json()
    except IntegrationError as exc:
        if "404" in str(exc):
            raise FlowNotFoundError(f"Workflow {flow_id} not found.") from exc
        raise HubSpotInfraError(str(exc)) from exc
    except (AuthenticationError, RateLimitError, TransientError) as exc:
        raise HubSpotInfraError(str(exc)) from exc


def create_flow(client: HubSpotClient, payload: Dict[str, Any]) -> Dict[str, Any]:
    require_automation_beta()
    try:
        resp = _request_with_scope_handling(client, "POST", AUTOMATION_BASE_URL, json_body=payload)
        return resp.json()
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotInfraError(str(exc)) from exc


def update_flow(client: HubSpotClient, flow_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
    require_automation_beta()
    url = f"{AUTOMATION_BASE_URL}/{flow_id}"
    try:
        resp = _request_with_scope_handling(client, "PUT", url, json_body=payload)
        return resp.json()
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotInfraError(str(exc)) from exc


def delete_flow(client: HubSpotClient, flow_id: str) -> None:
    require_automation_beta()
    url = f"{AUTOMATION_BASE_URL}/{flow_id}"
    try:
        _request_with_scope_handling(client, "DELETE", url)
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotInfraError(str(exc)) from exc


def diff_flow(local_payload: Dict[str, Any], remote_payload: Dict[str, Any]) -> str:
    local_dump = _canonical_json(local_payload)
    remote_dump = _canonical_json(remote_payload)
    if local_dump == remote_dump:
        return ""
    import difflib

    diff = difflib.unified_diff(
        remote_dump.splitlines(),
        local_dump.splitlines(),
        fromfile="remote",
        tofile="local",
        lineterm="",
    )
    return "\n".join(diff)


def load_flow_file(path: Path) -> Dict[str, Any]:
    if not path.exists():
        raise HubSpotInfraError(f"Workflow file not found: {path}")
    if path.suffix.lower() not in {".json"}:
        raise HubSpotInfraError("Workflow files must be JSON.")
    return json.loads(path.read_text(encoding="utf-8"))


def resolve_flow_id(payload: Dict[str, Any]) -> Optional[str]:
    return payload.get("id") or payload.get("flowId")


def resolve_v3_flow_id(client: HubSpotClient, v3_id: str) -> Optional[Dict[str, Any]]:
    """Attempt to resolve a v3 workflow ID to a v4 flow by matching ``flowId``."""
    flows = list_flows(client)
    v3_str = str(v3_id)
    flows_with_field = sum(1 for f in flows if f.get("flowId"))
    for flow in flows:
        if str(flow.get("flowId", "")) == v3_str:
            return flow
    logger.debug(
        "v3 resolution failed: %d/%d flows have flowId field",
        flows_with_field,
        len(flows),
    )
    return None


def get_flow_with_v3_fallback(
    client: HubSpotClient, flow_id: str
) -> Tuple[Dict[str, Any], bool]:
    """Get flow by ID, falling back to v3 ID resolution on 404.

    Returns ``(payload, was_v3_resolved)``.
    """
    try:
        return get_flow(client, flow_id), False
    except FlowNotFoundError:
        match = resolve_v3_flow_id(client, flow_id)
        if match:
            v4_id = match["id"]
            full = get_flow(client, v4_id)
            return full, True
        raise FlowNotFoundError(
            f"Workflow {flow_id} not found. "
            "If you copied this ID from the HubSpot UI, it may be a v3 workflow ID. "
            "Run `g-gremlin hubspot workflows ls` to find the v4 flow ID."
        )


def ensure_revision_or_raise(local_revision: Optional[str], latest_revision: Optional[str], force: bool) -> str:
    if not latest_revision:
        raise WorkflowApplyError("Latest revisionId missing from remote flow.")
    if not local_revision and not force:
        raise WorkflowApplyError("Local revisionId missing. Re-pull the flow or use --force.")
    if local_revision and local_revision != latest_revision and not force:
        raise WorkflowApplyError(
            f"Revision mismatch. Local={local_revision} Remote={latest_revision}. Use --force to override."
        )
    return latest_revision


def validate_flow_payload(payload: Dict[str, Any], *, mode: str) -> None:
    required: List[str]
    if mode == "create":
        required = ["type", "flowType", "actions"]
    elif mode == "update":
        required = ["type", "revisionId"]
    else:
        raise WorkflowApplyError(f"Unknown validation mode: {mode}")

    missing = [key for key in required if payload.get(key) in (None, "")]
    if missing:
        missing_str = ", ".join(missing)
        raise WorkflowApplyError(
            f"Workflow payload missing required fields: {missing_str}. "
            "Export a baseline with `g-gremlin hubspot workflows pull` first."
        )


def backup_flow(flow_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"{flow_id}-{timestamp}.json"
    path.write_text(_canonical_json(payload), encoding="utf-8")
    return path


def _paginate(client: HubSpotClient, url: str, params: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
    cursor: Optional[str] = None
    while True:
        page_params = dict(params)
        if cursor:
            page_params["after"] = cursor
        resp = _request_with_scope_handling(client, "GET", url, params=page_params)
        payload = resp.json()
        results = payload.get("results", []) or []
        for item in results:
            yield item
        paging = payload.get("paging", {}) or {}
        next_link = paging.get("next", {}) or {}
        cursor = next_link.get("after")
        if not cursor:
            break


def _canonical_json(payload: Dict[str, Any]) -> str:
    return json.dumps(payload, indent=2, sort_keys=True)
