"""
HubSpot lists helpers (read/export + optional create/update).
"""

from __future__ import annotations

import csv
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional

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

LISTS_BASE_URL = "https://api.hubapi.com/crm/v3/lists"


class HubSpotListsError(HubSpotInfraError):
    """Lists API error."""


@dataclass
class ListPlan:
    list_id: Optional[str]
    create: bool
    updates: Dict[str, Any]
    blocked: List[str]

    def to_dict(self) -> Dict[str, Any]:
        return {
            "list_id": self.list_id,
            "create": self.create,
            "updates": self.updates,
            "blocked": self.blocked,
        }


def list_lists(client: HubSpotClient, limit: Optional[int] = None) -> List[Dict[str, Any]]:
    params = {"count": min(limit or 500, 500)}
    lists: List[Dict[str, Any]] = []
    for item in _paginate_search(client, params):
        lists.append(item)
        if limit and len(lists) >= limit:
            break
    return lists


def get_list(client: HubSpotClient, list_id: str) -> Dict[str, Any]:
    url = f"{LISTS_BASE_URL}/{list_id}"
    try:
        resp = _request_with_scope_handling(client, "GET", url)
        payload = resp.json()
        return payload.get("list") or payload
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotListsError(str(exc)) from exc


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


def update_list(client: HubSpotClient, list_id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
    updates = payload or {}
    result: Dict[str, Any] = {}
    if "name" in updates or "listName" in updates:
        url = f"{LISTS_BASE_URL}/{list_id}/name"
        name = updates.get("name") or updates.get("listName")
        resp = _request_with_scope_handling(client, "PUT", url, json_body={"name": name})
        result["name"] = resp.json()
    if "filterBranch" in updates:
        url = f"{LISTS_BASE_URL}/{list_id}/filterBranch"
        resp = _request_with_scope_handling(
            client,
            "PUT",
            url,
            json_body={"filterBranch": updates["filterBranch"]},
        )
        result["filterBranch"] = resp.json()
    return result


def list_members(
    client: HubSpotClient,
    list_id: str,
    *,
    limit: Optional[int] = None,
) -> Iterable[Dict[str, Any]]:
    url = f"{LISTS_BASE_URL}/{list_id}/memberships"
    params = {"limit": min(limit or 500, 500)}
    count = 0
    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
            count += 1
            if limit and count >= limit:
                return
        paging = payload.get("paging", {}) or {}
        next_link = paging.get("next", {}) or {}
        cursor = next_link.get("after")
        if not cursor:
            break


def export_members_to_csv(
    list_id: str,
    members: Iterable[Dict[str, Any]],
    output_path: Path,
) -> int:
    output_path.parent.mkdir(parents=True, exist_ok=True)
    rows = []
    for member in members:
        rows.append(
            {
                "list_id": str(list_id),
                "record_id": member.get("recordId") or member.get("record_id"),
                "membership_timestamp": member.get("membershipTimestamp")
                or member.get("membership_timestamp"),
            }
        )
    with output_path.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(
            f,
            fieldnames=["list_id", "record_id", "membership_timestamp"],
        )
        writer.writeheader()
        writer.writerows(rows)
    return len(rows)


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


def export_list_spec(list_payload: Dict[str, Any]) -> Dict[str, Any]:
    keys = [
        "listId",
        "name",
        "objectTypeId",
        "processingType",
        "filterBranch",
    ]
    spec = {k: list_payload.get(k) for k in keys if k in list_payload}
    return spec


def plan_list_changes(local_payload: Dict[str, Any], remote_payload: Optional[Dict[str, Any]]) -> ListPlan:
    list_id = str(local_payload.get("listId") or local_payload.get("id") or "") or None
    create = remote_payload is None
    updates: Dict[str, Any] = {}
    blocked: List[str] = []

    if create:
        return ListPlan(list_id=list_id, create=True, updates=local_payload, blocked=blocked)

    if not remote_payload:
        raise HubSpotListsError("Remote list payload missing.")

    if local_payload.get("objectTypeId") and local_payload.get("objectTypeId") != remote_payload.get("objectTypeId"):
        blocked.append("objectTypeId")
    if local_payload.get("processingType") and local_payload.get("processingType") != remote_payload.get("processingType"):
        blocked.append("processingType")

    if "name" in local_payload and local_payload.get("name") != remote_payload.get("name"):
        updates["name"] = local_payload.get("name")
    if "filterBranch" in local_payload and local_payload.get("filterBranch") != remote_payload.get("filterBranch"):
        updates["filterBranch"] = local_payload.get("filterBranch")

    return ListPlan(list_id=list_id, create=False, updates=updates, blocked=blocked)


def backup_list(list_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"{list_id}-{timestamp}.json"
    path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
    return path


def diff_lists(local_payload: Dict[str, Any], remote_payload: Dict[str, Any]) -> str:
    local_dump = json.dumps(local_payload, indent=2, sort_keys=True)
    remote_dump = json.dumps(remote_payload, indent=2, sort_keys=True)
    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 _validate_manual_or_snapshot(list_payload: Dict[str, Any]) -> None:
    """Raise if the list is not MANUAL or SNAPSHOT (membership edits are only
    allowed on those processing types)."""
    ptype = (list_payload.get("processingType") or "").upper()
    if ptype not in ("MANUAL", "SNAPSHOT"):
        raise HubSpotListsError(
            f"List processingType is '{ptype}'. Membership add/remove only works "
            "on MANUAL or SNAPSHOT lists — dynamic lists are managed by their "
            "filter rules and cannot have members added directly."
        )


def add_members(
    client: HubSpotClient,
    list_id: str,
    record_ids: List[str],
) -> Dict[str, Any]:
    """Add record IDs to a static (MANUAL/SNAPSHOT) list.

    Uses PUT /crm/v3/lists/{listId}/memberships/add
    """
    list_payload = get_list(client, list_id)
    _validate_manual_or_snapshot(list_payload)

    url = f"{LISTS_BASE_URL}/{list_id}/memberships/add"
    try:
        resp = _request_with_scope_handling(
            client, "PUT", url, json_body=record_ids,
        )
        return resp.json()
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotListsError(str(exc)) from exc


def remove_members(
    client: HubSpotClient,
    list_id: str,
    record_ids: List[str],
) -> Dict[str, Any]:
    """Remove record IDs from a static (MANUAL/SNAPSHOT) list.

    Uses PUT /crm/v3/lists/{listId}/memberships/remove
    """
    list_payload = get_list(client, list_id)
    _validate_manual_or_snapshot(list_payload)

    url = f"{LISTS_BASE_URL}/{list_id}/memberships/remove"
    try:
        resp = _request_with_scope_handling(
            client, "PUT", url, json_body=record_ids,
        )
        return resp.json()
    except (AuthenticationError, IntegrationError, RateLimitError, TransientError) as exc:
        raise HubSpotListsError(str(exc)) from exc


def load_record_ids_from_csv(path: Path, column: str = "record_id") -> List[str]:
    """Read record IDs from a CSV column, stripping whitespace."""
    if not path.exists():
        raise HubSpotListsError(f"CSV file not found: {path}")
    ids: List[str] = []
    with path.open("r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        if column not in (reader.fieldnames or []):
            raise HubSpotListsError(
                f"Column '{column}' not found in {path}. "
                f"Available columns: {', '.join(reader.fieldnames or [])}"
            )
        for row in reader:
            val = (row.get(column) or "").strip()
            if val:
                ids.append(val)
    return ids


def _paginate_search(client: HubSpotClient, params: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
    offset: Optional[int] = None
    while True:
        body = {"count": params.get("count", 500)}
        if offset is not None:
            body["offset"] = offset
        resp = _request_with_scope_handling(client, "POST", f"{LISTS_BASE_URL}/search", json_body=body)
        payload = resp.json()
        results = payload.get("lists", []) or payload.get("results", []) or []
        for item in results:
            yield item
        if payload.get("hasMore"):
            offset = payload.get("offset")
            continue
        break
