"""Optional AI provider registration and client for field mapping second-pass."""

from __future__ import annotations

import base64
import json
import os
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any, Literal
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

from pydantic import BaseModel, ConfigDict, computed_field

from g_gremlin.autopilot.discovery import FieldMappingSuggestion
from g_gremlin.autopilot.token_store import TokenStore

ProviderType = Literal["anthropic", "openai", "google"]
AuthModeType = Literal["api_key", "oauth"]
WorkflowId = Literal["deal_support", "quote_intelligence", "close_certification", "document_reconciliation"]


def _utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _parse_iso_utc(value: str) -> datetime | None:
    if not value:
        return None
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except Exception:
        return None
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _iso_from_now(seconds_from_now: int) -> str:
    return (datetime.now(timezone.utc) + timedelta(seconds=max(1, int(seconds_from_now)))).isoformat()


def _coerce_positive_int(value: Any) -> int | None:
    if value in (None, ""):
        return None
    try:
        parsed = int(str(value))
    except Exception:
        return None
    return parsed if parsed > 0 else None


def _google_oauth_client_id() -> str:
    return (
        os.getenv("G_GREMLIN_AI_GOOGLE_OAUTH_CLIENT_ID", "").strip()
        or os.getenv("G_GREMLIN_GOOGLE_OAUTH_CLIENT_ID", "").strip()
    )


def _google_oauth_client_secret() -> str:
    return (
        os.getenv("G_GREMLIN_AI_GOOGLE_OAUTH_CLIENT_SECRET", "").strip()
        or os.getenv("G_GREMLIN_GOOGLE_OAUTH_CLIENT_SECRET", "").strip()
    )


def _google_oauth_token_url() -> str:
    return os.getenv("G_GREMLIN_AI_GOOGLE_OAUTH_TOKEN_URL", "").strip() or "https://oauth2.googleapis.com/token"


def _post_google_form_json(url: str, form_payload: dict[str, str]) -> dict[str, Any]:
    encoded = urlencode(form_payload).encode("utf-8")
    req = Request(
        url,
        data=encoded,
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
        },
        method="POST",
    )
    try:
        with urlopen(req, timeout=20) as response:
            body = response.read().decode("utf-8", errors="replace")
    except HTTPError as exc:
        details = exc.read().decode("utf-8", errors="replace")
        message = details.strip() or str(exc.reason)
        raise ValueError(f"Google OAuth token exchange failed: {message}") from exc
    except URLError as exc:
        raise ValueError(f"Google OAuth token exchange failed: {exc.reason}") from exc

    try:
        payload = json.loads(body) if body.strip() else {}
    except Exception as exc:
        raise ValueError(f"Google OAuth token exchange returned invalid JSON: {exc}") from exc
    if not isinstance(payload, dict):
        raise ValueError("Google OAuth token exchange returned unexpected payload type")
    if payload.get("error"):
        raise ValueError(str(payload.get("error_description") or payload.get("error")))
    return payload


def _refresh_google_oauth_access_token(
    *,
    refresh_token: str,
    client_id: str,
    client_secret: str,
) -> dict[str, Any]:
    return _post_google_form_json(
        _google_oauth_token_url(),
        {
            "grant_type": "refresh_token",
            "client_id": client_id,
            "client_secret": client_secret,
            "refresh_token": refresh_token,
        },
    )


class AIProviderConfig(BaseModel):
    model_config = ConfigDict(extra="ignore")

    provider: ProviderType
    api_key: str = ""
    model: str
    enabled: bool = True
    auth_mode: AuthModeType = "api_key"
    access_token: str | None = None
    refresh_token: str | None = None
    token_type: str | None = None
    expires_at: str | None = None


class AIProviderStatus(BaseModel):
    model_config = ConfigDict(extra="ignore")

    configured: bool
    provider: ProviderType | None = None
    model: str | None = None
    enabled: bool = False
    auth_mode: AuthModeType | None = None
    oauth_connected: bool = False
    oauth_expires_at: str | None = None
    last_test_ok: bool | None = None
    last_test_at: str | None = None
    error: str | None = None


class ModelCapabilityProfileV1(BaseModel):
    model_config = ConfigDict(extra="ignore")

    version: int = 1
    connection_id: Literal["primary", "document"] = "primary"
    provider: ProviderType
    model: str
    enabled: bool = True
    supports_structured_output: bool = False
    supports_document_inputs: bool = False
    capability_source: Literal["default", "declared", "verified"] = "default"

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "ModelCapabilityProfile/v1"


class WorkflowModelRequirementV1(BaseModel):
    model_config = ConfigDict(extra="ignore")

    version: int = 1
    workflow: WorkflowId
    preferred_connection_id: Literal["primary", "document"] = "primary"
    requires_enabled_connection: bool = True
    requires_structured_output: bool = True
    requires_document_inputs: bool = False

    @computed_field(alias="$schema")
    @property
    def schema(self) -> str:
        return "WorkflowModelRequirement/v1"


class AIProviderStore:
    _INTEGRATION_KEY = "ai_provider"

    def __init__(self, state_dir: Path) -> None:
        self.state_dir = Path(state_dir)
        self.token_store = TokenStore(self.state_dir)

    def save_config(self, config: AIProviderConfig) -> None:
        existing = self.token_store.load_tokens(self._INTEGRATION_KEY) or {}
        payload = {
            "provider": config.provider,
            "api_key": config.api_key,
            "model": config.model,
            "enabled": bool(config.enabled),
            "auth_mode": "api_key",
            "access_token": None,
            "refresh_token": None,
            "token_type": None,
            "expires_at": None,
            "last_test_ok": existing.get("last_test_ok"),
            "last_test_at": existing.get("last_test_at"),
            "last_test_error": existing.get("last_test_error"),
        }
        self.token_store.save_tokens(self._INTEGRATION_KEY, payload)

    def save_oauth_config(self, config: AIProviderConfig) -> None:
        existing = self.token_store.load_tokens(self._INTEGRATION_KEY) or {}
        payload = {
            "provider": config.provider,
            "api_key": "",
            "model": config.model,
            "enabled": bool(config.enabled),
            "auth_mode": "oauth",
            "access_token": str(config.access_token or "").strip() or None,
            "refresh_token": str(config.refresh_token or "").strip() or None,
            "token_type": str(config.token_type or "").strip() or None,
            "expires_at": str(config.expires_at or "").strip() or None,
            "last_test_ok": existing.get("last_test_ok"),
            "last_test_at": existing.get("last_test_at"),
            "last_test_error": existing.get("last_test_error"),
        }
        self.token_store.save_tokens(self._INTEGRATION_KEY, payload)

    def update_oauth_settings(self, *, provider: ProviderType, model: str, enabled: bool) -> bool:
        payload = self.token_store.load_tokens(self._INTEGRATION_KEY) or {}
        auth_mode = str(payload.get("auth_mode") or "").strip().lower()
        current_provider = str(payload.get("provider") or "").strip().lower()
        access_token = str(payload.get("access_token") or "").strip()
        if auth_mode != "oauth" or current_provider != "google" or provider != "google" or not access_token:
            return False
        payload["provider"] = provider
        payload["model"] = model
        payload["enabled"] = bool(enabled)
        self.token_store.save_tokens(self._INTEGRATION_KEY, payload)
        return True

    def load_config(self) -> AIProviderConfig | None:
        payload = self.token_store.load_tokens(self._INTEGRATION_KEY)
        if not payload:
            return None

        provider = str(payload.get("provider") or "").strip().lower()
        model = str(payload.get("model") or "").strip()
        if provider not in {"anthropic", "openai", "google"} or not model:
            return None

        auth_mode = str(payload.get("auth_mode") or "api_key").strip().lower()
        if auth_mode == "oauth":
            refreshed_payload = self._maybe_refresh_google_oauth_tokens(payload)
            access_token = str(refreshed_payload.get("access_token") or "").strip()
            if provider != "google" or not access_token:
                return None
            return AIProviderConfig(
                provider=provider,  # type: ignore[arg-type]
                api_key="",
                model=model,
                enabled=bool(refreshed_payload.get("enabled", True)),
                auth_mode="oauth",
                access_token=access_token,
                refresh_token=str(refreshed_payload.get("refresh_token") or "").strip() or None,
                token_type=str(refreshed_payload.get("token_type") or "").strip() or None,
                expires_at=str(refreshed_payload.get("expires_at") or "").strip() or None,
            )

        api_key = str(payload.get("api_key") or "").strip()
        if not api_key:
            return None
        return AIProviderConfig(
            provider=provider,  # type: ignore[arg-type]
            api_key=api_key,
            model=model,
            enabled=bool(payload.get("enabled", True)),
            auth_mode="api_key",
        )

    def delete_config(self) -> bool:
        return self.token_store.delete_tokens(self._INTEGRATION_KEY)

    def load_status(self) -> AIProviderStatus:
        payload = self.token_store.load_tokens(self._INTEGRATION_KEY)
        if not payload:
            return AIProviderStatus(configured=False)

        provider = str(payload.get("provider") or "").strip().lower()
        model = str(payload.get("model") or "").strip()
        if provider not in {"anthropic", "openai", "google"} or not model:
            return AIProviderStatus(configured=False)
        auth_mode = str(payload.get("auth_mode") or "api_key").strip().lower()
        api_key = str(payload.get("api_key") or "").strip()
        access_token = str(payload.get("access_token") or "").strip()
        if auth_mode == "oauth":
            configured = provider == "google" and bool(access_token)
        else:
            configured = bool(api_key)

        return AIProviderStatus(
            configured=configured,
            provider=provider,  # type: ignore[arg-type]
            model=model,
            enabled=bool(payload.get("enabled", True)),
            auth_mode=auth_mode if auth_mode in {"api_key", "oauth"} else "api_key",
            oauth_connected=provider == "google" and bool(access_token),
            oauth_expires_at=str(payload.get("expires_at") or "").strip() or None,
            last_test_ok=payload.get("last_test_ok")
            if isinstance(payload.get("last_test_ok"), bool)
            else None,
            last_test_at=str(payload.get("last_test_at") or "").strip() or None,
            error=str(payload.get("last_test_error") or "").strip() or None,
        )

    def save_test_result(self, ok: bool, error: str | None) -> None:
        payload = self.token_store.load_tokens(self._INTEGRATION_KEY)
        if not payload:
            return
        payload["last_test_ok"] = bool(ok)
        payload["last_test_at"] = _utc_now_iso()
        payload["last_test_error"] = error
        self.token_store.save_tokens(self._INTEGRATION_KEY, payload)

    def _maybe_refresh_google_oauth_tokens(self, payload: dict[str, Any]) -> dict[str, Any]:
        provider = str(payload.get("provider") or "").strip().lower()
        auth_mode = str(payload.get("auth_mode") or "").strip().lower()
        if provider != "google" or auth_mode != "oauth":
            return payload

        refresh_token = str(payload.get("refresh_token") or "").strip()
        if not refresh_token:
            return payload

        expires_at_raw = str(payload.get("expires_at") or "").strip()
        expires_at = _parse_iso_utc(expires_at_raw) if expires_at_raw else None
        if expires_at is not None and (expires_at - datetime.now(timezone.utc)).total_seconds() > 120:
            return payload

        client_id = _google_oauth_client_id()
        client_secret = _google_oauth_client_secret()
        if not client_id or not client_secret:
            return payload

        try:
            refreshed = _refresh_google_oauth_access_token(
                refresh_token=refresh_token,
                client_id=client_id,
                client_secret=client_secret,
            )
        except Exception:
            return payload

        access_token = str(refreshed.get("access_token") or "").strip()
        if not access_token:
            return payload

        updated = dict(payload)
        updated["access_token"] = access_token
        updated["token_type"] = str(refreshed.get("token_type") or updated.get("token_type") or "").strip() or None
        expires_in = _coerce_positive_int(refreshed.get("expires_in"))
        if expires_in is not None:
            updated["expires_at"] = _iso_from_now(expires_in)
        new_refresh_token = str(refreshed.get("refresh_token") or "").strip()
        if new_refresh_token:
            updated["refresh_token"] = new_refresh_token
        self.token_store.save_tokens(self._INTEGRATION_KEY, updated)
        return updated


def build_model_capability_profile(
    config: AIProviderConfig,
    *,
    connection_id: Literal["primary", "document"] = "primary",
    supports_structured_output: bool = False,
    supports_document_inputs: bool = False,
    capability_source: Literal["default", "declared", "verified"] = "default",
) -> ModelCapabilityProfileV1:
    return ModelCapabilityProfileV1(
        connection_id=connection_id,
        provider=config.provider,
        model=config.model,
        enabled=bool(config.enabled),
        supports_structured_output=bool(supports_structured_output),
        supports_document_inputs=bool(supports_document_inputs),
        capability_source=capability_source,
    )


def workflow_model_requirement(workflow: WorkflowId) -> WorkflowModelRequirementV1:
    defaults: dict[WorkflowId, dict[str, Any]] = {
        "deal_support": {
            "preferred_connection_id": "primary",
            "requires_enabled_connection": True,
            "requires_structured_output": False,
            "requires_document_inputs": False,
        },
        "quote_intelligence": {
            "preferred_connection_id": "primary",
            "requires_enabled_connection": True,
            "requires_structured_output": True,
            "requires_document_inputs": False,
        },
        "close_certification": {
            "preferred_connection_id": "primary",
            "requires_enabled_connection": True,
            "requires_structured_output": True,
            "requires_document_inputs": False,
        },
        "document_reconciliation": {
            "preferred_connection_id": "document",
            "requires_enabled_connection": True,
            "requires_structured_output": True,
            "requires_document_inputs": True,
        },
    }
    return WorkflowModelRequirementV1(workflow=workflow, **defaults[workflow])


def model_satisfies_workflow(
    profile: ModelCapabilityProfileV1,
    requirement: WorkflowModelRequirementV1,
) -> bool:
    if requirement.requires_enabled_connection and not profile.enabled:
        return False
    if requirement.requires_structured_output and not profile.supports_structured_output:
        return False
    if requirement.requires_document_inputs and not profile.supports_document_inputs:
        return False
    return True


def test_ai_provider(config: AIProviderConfig) -> tuple[bool, str | None]:
    if config.provider == "google":
        try:
            _call_google_provider(config=config, prompt="Output exactly one word: pong", max_output_tokens=64)
            return True, None
        except Exception as exc:  # pragma: no cover - covered via mocks
            # Some preview Gemini models may consume the initial token budget
            # without returning visible text. Retry once with a larger cap.
            if "finish_reason: MAX_TOKENS" in str(exc):
                try:
                    _call_google_provider(
                        config=config,
                        prompt="Output exactly one word: pong",
                        max_output_tokens=256,
                    )
                    return True, None
                except Exception as retry_exc:
                    return False, _normalize_provider_error(retry_exc)
            return False, _normalize_provider_error(exc)

    if config.provider == "anthropic":
        try:
            import anthropic  # type: ignore
        except ImportError:
            return False, "anthropic package not installed"
        try:
            client = anthropic.Anthropic(api_key=config.api_key)
            client.messages.create(
                model=config.model,
                max_tokens=1,
                messages=[{"role": "user", "content": "ping"}],
            )
            return True, None
        except Exception as exc:  # pragma: no cover - covered via mocks
            return False, _normalize_provider_error(exc)

    try:
        import openai  # type: ignore
    except ImportError:
        return False, "openai package not installed"
    try:
        client = openai.OpenAI(api_key=config.api_key)
        client.chat.completions.create(
            model=config.model,
            max_tokens=1,
            messages=[{"role": "user", "content": "ping"}],
        )
        return True, None
    except Exception as exc:  # pragma: no cover - covered via mocks
        return False, _normalize_provider_error(exc)


def ai_field_mapping_pass(
    config: AIProviderConfig,
    suggestions: list[dict[str, Any]],
    object_profiles: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    if not config.enabled:
        return []
    if not suggestions:
        return []

    try:
        prompt = _mapping_prompt(suggestions, object_profiles)
        raw_response = _call_provider_for_adjustments(config, prompt)
        return _parse_adjustments(raw_response)
    except Exception:
        return []


def ai_generate_text(
    config: AIProviderConfig,
    prompt: str,
    *,
    max_output_tokens: int = 700,
) -> str:
    if config.provider == "google":
        return _call_google_provider(
            config=config,
            prompt=prompt,
            max_output_tokens=max_output_tokens,
        )

    if config.provider == "anthropic":
        try:
            import anthropic  # type: ignore
        except ImportError:
            raise RuntimeError("anthropic package not installed")
        client = anthropic.Anthropic(api_key=config.api_key)
        response = client.messages.create(
            model=config.model,
            max_tokens=max_output_tokens,
            messages=[{"role": "user", "content": prompt}],
        )
        return _extract_anthropic_text(response)

    try:
        import openai  # type: ignore
    except ImportError:
        raise RuntimeError("openai package not installed")
    client = openai.OpenAI(api_key=config.api_key)
    response = client.chat.completions.create(
        model=config.model,
        max_tokens=max_output_tokens,
        messages=[{"role": "user", "content": prompt}],
    )
    return _extract_openai_chat_text(response)


def ai_generate_document_text(
    config: AIProviderConfig,
    prompt: str,
    *,
    document_bytes: bytes,
    filename: str,
    document_mime_type: str = "application/pdf",
    max_output_tokens: int = 1200,
) -> str:
    normalized_filename = str(filename or "").strip() or "document.pdf"
    normalized_mime_type = str(document_mime_type or "").strip() or "application/pdf"
    if not document_bytes:
        raise ValueError("Document bytes are required")

    if config.provider == "google":
        return _call_google_document_provider(
            config=config,
            prompt=prompt,
            document_bytes=document_bytes,
            document_mime_type=normalized_mime_type,
            max_output_tokens=max_output_tokens,
        )

    if config.provider == "anthropic":
        try:
            import anthropic  # type: ignore
        except ImportError:
            raise RuntimeError("anthropic package not installed")
        client = anthropic.Anthropic(api_key=config.api_key)
        response = client.messages.create(
            model=config.model,
            max_tokens=max_output_tokens,
            messages=[
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "document",
                            "source": {
                                "type": "base64",
                                "media_type": normalized_mime_type,
                                "data": base64.b64encode(document_bytes).decode("ascii"),
                            },
                        },
                        {
                            "type": "text",
                            "text": prompt,
                        },
                    ],
                }
            ],
        )
        return _extract_anthropic_text(response)

    try:
        import openai  # type: ignore
    except ImportError:
        raise RuntimeError("openai package not installed")
    client = openai.OpenAI(api_key=config.api_key)
    response = client.chat.completions.create(
        model=config.model,
        max_tokens=max_output_tokens,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "file",
                        "file": {
                            "filename": normalized_filename,
                            "file_data": (
                                f"data:{normalized_mime_type};base64,"
                                f"{base64.b64encode(document_bytes).decode('ascii')}"
                            ),
                        },
                    },
                    {
                        "type": "text",
                        "text": prompt,
                    },
                ],
            }
        ],
    )
    return _extract_openai_chat_text(response)


def _extract_openai_chat_text(response: Any) -> str:
    choices = getattr(response, "choices", None) or []
    if not choices:
        raise ValueError("AI response missing choices")
    message = getattr(choices[0], "message", None)
    content = getattr(message, "content", None)
    if not isinstance(content, str) or not content.strip():
        if isinstance(content, list):
            parts: list[str] = []
            for item in content:
                if isinstance(item, dict):
                    text = item.get("text")
                    if isinstance(text, str) and text.strip():
                        parts.append(text.strip())
                    continue
                text = getattr(item, "text", None)
                if isinstance(text, str) and text.strip():
                    parts.append(text.strip())
            merged = "\n".join(parts).strip()
            if merged:
                return merged
        raise ValueError("AI response content is empty")
    return content


def _call_provider_for_adjustments(config: AIProviderConfig, prompt: str) -> str:
    return ai_generate_text(config, prompt, max_output_tokens=700)


def _call_google_provider(*, config: AIProviderConfig, prompt: str, max_output_tokens: int) -> str:
    model_name = quote(config.model.strip(), safe="")
    if not model_name:
        raise ValueError("Google model is required")
    headers = {"Content-Type": "application/json"}
    if config.auth_mode == "oauth":
        access_token = str(config.access_token or "").strip()
        if not access_token:
            raise ValueError("Google OAuth access token is required")
        endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent"
        headers["Authorization"] = f"Bearer {access_token}"
    else:
        api_key = quote(config.api_key.strip(), safe="")
        if not api_key:
            raise ValueError("Google API key is required")
        endpoint = (
            f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent"
            f"?key={api_key}"
        )
    payload = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {"maxOutputTokens": max_output_tokens},
    }
    body = json.dumps(payload).encode("utf-8")
    request = Request(
        endpoint,
        data=body,
        headers=headers,
        method="POST",
    )
    try:
        with urlopen(request, timeout=30) as response:  # noqa: S310 - trusted Google API endpoint
            raw = response.read().decode("utf-8")
    except HTTPError as exc:
        detail = ""
        try:
            detail = exc.read().decode("utf-8").strip()
        except Exception:
            detail = ""
        message = detail or str(exc).strip() or "Google API request failed"
        raise ValueError(message) from exc
    except URLError as exc:
        raise ValueError(str(exc).strip() or "Google API request failed") from exc

    try:
        parsed = json.loads(raw)
    except Exception as exc:
        raise ValueError("Google API returned invalid JSON") from exc
    return _extract_google_text(parsed)


def _call_google_document_provider(
    *,
    config: AIProviderConfig,
    prompt: str,
    document_bytes: bytes,
    document_mime_type: str,
    max_output_tokens: int,
) -> str:
    model_name = quote(config.model.strip(), safe="")
    if not model_name:
        raise ValueError("Google model is required")
    headers = {"Content-Type": "application/json"}
    if config.auth_mode == "oauth":
        access_token = str(config.access_token or "").strip()
        if not access_token:
            raise ValueError("Google OAuth access token is required")
        endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent"
        headers["Authorization"] = f"Bearer {access_token}"
    else:
        api_key = quote(config.api_key.strip(), safe="")
        if not api_key:
            raise ValueError("Google API key is required")
        endpoint = (
            f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent"
            f"?key={api_key}"
        )
    payload = {
        "contents": [
            {
                "parts": [
                    {
                        "inline_data": {
                            "mime_type": document_mime_type,
                            "data": base64.b64encode(document_bytes).decode("ascii"),
                        }
                    },
                    {"text": prompt},
                ]
            }
        ],
        "generationConfig": {"maxOutputTokens": max_output_tokens},
    }
    body = json.dumps(payload).encode("utf-8")
    request = Request(
        endpoint,
        data=body,
        headers=headers,
        method="POST",
    )
    try:
        with urlopen(request, timeout=60) as response:  # noqa: S310 - trusted Google API endpoint
            raw = response.read().decode("utf-8")
    except HTTPError as exc:
        detail = ""
        try:
            detail = exc.read().decode("utf-8").strip()
        except Exception:
            detail = ""
        message = detail or str(exc).strip() or "Google API request failed"
        raise ValueError(message) from exc
    except URLError as exc:
        raise ValueError(str(exc).strip() or "Google API request failed") from exc

    try:
        parsed = json.loads(raw)
    except Exception as exc:
        raise ValueError("Google API returned invalid JSON") from exc
    return _extract_google_text(parsed)


def _extract_google_text(response_payload: dict[str, Any]) -> str:
    if not isinstance(response_payload, dict):
        raise ValueError("AI response payload is invalid")

    candidates = response_payload.get("candidates")
    if not isinstance(candidates, list) or not candidates:
        raise ValueError("AI response missing candidates")

    text_parts: list[str] = []
    finish_reasons: list[str] = []
    for candidate in candidates:
        if not isinstance(candidate, dict):
            continue
        finish_reason = str(candidate.get("finishReason") or "").strip()
        if finish_reason:
            finish_reasons.append(finish_reason)
        content = candidate.get("content")
        if not isinstance(content, dict):
            continue
        parts = content.get("parts")
        if not isinstance(parts, list):
            continue
        for part in parts:
            if not isinstance(part, dict):
                continue
            text = part.get("text")
            if isinstance(text, str) and text.strip():
                text_parts.append(text.strip())

    merged = "\n".join(text_parts).strip()
    if merged:
        return merged

    prompt_feedback = response_payload.get("promptFeedback")
    if isinstance(prompt_feedback, dict):
        block_reason = str(prompt_feedback.get("blockReason") or "").strip()
        if block_reason:
            message = str(prompt_feedback.get("blockReasonMessage") or "").strip()
            if message:
                raise ValueError(f"AI response blocked ({block_reason}): {message}")
            raise ValueError(f"AI response blocked ({block_reason})")

    if finish_reasons:
        normalized_reasons = sorted({reason for reason in finish_reasons if reason})
        if normalized_reasons:
            raise ValueError(f"AI response content is empty (finish_reason: {', '.join(normalized_reasons)})")
    raise ValueError("AI response content is empty")


def _extract_anthropic_text(response: Any) -> str:
    content_items = getattr(response, "content", None) or []
    parts: list[str] = []
    for item in content_items:
        text = getattr(item, "text", None)
        if isinstance(text, str) and text.strip():
            parts.append(text)
    merged = "\n".join(parts).strip()
    if not merged:
        raise ValueError("AI response content is empty")
    return merged


def _mapping_prompt(
    suggestions: list[dict[str, Any]],
    object_profiles: list[dict[str, Any]],
) -> str:
    return (
        "You are validating ambiguous CRM field mapping suggestions.\n"
        "Given the object profile and ambiguous mapping candidates, return ONLY JSON.\n"
        "Return an array with items in this exact shape:\n"
        '{"target":"...", "source_field":"...", "confidence_adjustment":0.0, "rationale":"..."}\n'
        "Rules:\n"
        "- Include at most one item per input suggestion.\n"
        "- confidence_adjustment is a float between -0.5 and +0.5.\n"
        "- Do not invent new targets or source fields.\n\n"
        f"Object profiles:\n{json.dumps(object_profiles, indent=2)}\n\n"
        f"Ambiguous suggestions:\n{json.dumps(suggestions, indent=2)}\n"
    )


def _parse_adjustments(raw_response: str) -> list[dict[str, Any]]:
    text = raw_response.strip()
    if not text:
        return []
    if "```" in text:
        text = _strip_markdown_fence(text)

    parsed: Any
    try:
        parsed = json.loads(text)
    except Exception:
        start = text.find("[")
        end = text.rfind("]")
        if start < 0 or end < start:
            return []
        try:
            parsed = json.loads(text[start : end + 1])
        except Exception:
            return []

    if isinstance(parsed, dict) and isinstance(parsed.get("adjustments"), list):
        parsed = parsed["adjustments"]
    if not isinstance(parsed, list):
        return []

    normalized: list[dict[str, Any]] = []
    for item in parsed:
        if not isinstance(item, dict):
            continue
        target = str(item.get("target") or "").strip()
        source_field = str(item.get("source_field") or "").strip()
        rationale = str(item.get("rationale") or "").strip()
        if not target or not source_field:
            continue
        try:
            delta = float(item.get("confidence_adjustment", 0.0))
        except Exception:
            continue
        normalized.append(
            {
                "target": target,
                "source_field": source_field,
                "confidence_adjustment": delta,
                "rationale": rationale,
            }
        )
    return normalized


def _strip_markdown_fence(text: str) -> str:
    stripped = text.strip()
    if stripped.startswith("```"):
        first_newline = stripped.find("\n")
        if first_newline >= 0:
            stripped = stripped[first_newline + 1 :]
    if stripped.endswith("```"):
        stripped = stripped[:-3]
    return stripped.strip()


def _normalize_provider_error(exc: Exception) -> str:
    name = exc.__class__.__name__.lower()
    message = str(exc).strip() or exc.__class__.__name__
    if "authentication" in name or name == "autherror":
        return f"Authentication failed: {message}"
    if "notfound" in name or "not_found" in name:
        return f"Model not found: {message}"
    return message


def _coerce_suggestions(
    suggestions: list[FieldMappingSuggestion] | list[dict[str, Any]],
) -> list[dict[str, Any]]:
    normalized: list[dict[str, Any]] = []
    for item in suggestions:
        if isinstance(item, FieldMappingSuggestion):
            normalized.append(item.model_dump(exclude_none=False))
        elif isinstance(item, dict):
            normalized.append(dict(item))
    return normalized


__all__ = [
    "ProviderType",
    "WorkflowId",
    "AIProviderConfig",
    "AIProviderStatus",
    "ModelCapabilityProfileV1",
    "WorkflowModelRequirementV1",
    "AIProviderStore",
    "build_model_capability_profile",
    "workflow_model_requirement",
    "model_satisfies_workflow",
    "test_ai_provider",
    "ai_field_mapping_pass",
    "ai_generate_document_text",
    "ai_generate_text",
]
