"""External knowledge ingest API client and draft-level orchestration."""
from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any, Callable

import httpx

from core.config import IngestApiConfig
from core.text_limits import ERROR_MESSAGE_MAX_CHARS, clip_text
from decode_content.models import IngestRecord, PayloadDraft
from decode_content.payloads import IngestPayloadValidationError, validate_ingest_payload
from decode_content.repository import DecodeRepository

PostFn = Callable[..., Any]


class IngestApiError(RuntimeError):
    def __init__(
        self,
        message: str,
        *,
        response_payload: dict[str, Any] | None = None,
        http_status: int | None = None,
        attempt_count: int = 1,
    ) -> None:
        super().__init__(message)
        self.response_payload = response_payload or {}
        self.http_status = http_status
        self.attempt_count = attempt_count


@dataclass(frozen=True)
class IngestApiResult:
    knowledge_id: str | None
    source_id: str | None
    http_status: int
    attempt_count: int
    response_payload: dict[str, Any]


class KnowledgeIngestClient:
    """Small synchronous client for POST /api/v1/knowledge/ingest."""

    def __init__(
        self,
        config: IngestApiConfig,
        *,
        post_fn: PostFn | None = None,
        sleep_fn: Callable[[float], None] = time.sleep,
    ) -> None:
        self.config = config
        self.post_fn = post_fn or httpx.post
        self.sleep_fn = sleep_fn

    def ingest(self, payload: dict[str, Any]) -> IngestApiResult:
        validate_ingest_payload(payload)
        if not self.config.url:
            raise IngestApiError(
                "CK_INGEST_API_URL 未配置，真实 ingest 已关闭",
                response_payload={"dry_run": False, "missing_config": "CK_INGEST_API_URL"},
            )
        headers = {"Content-Type": "application/json"}
        if self.config.token:
            headers["Authorization"] = f"Bearer {self.config.token}"
        delays = tuple(self.config.retry_delays_seconds)
        last_error: IngestApiError | None = None
        for attempt_index in range(len(delays) + 1):
            attempt_count = attempt_index + 1
            try:
                response = self.post_fn(
                    self.config.url,
                    json=payload,
                    headers=headers,
                    timeout=self.config.timeout_seconds,
                )
                raw = _response_payload(response)
                status_code = int(getattr(response, "status_code", 0) or 0)
                if 200 <= status_code < 300:
                    knowledge_id = raw.get("knowledge_id") if isinstance(raw, dict) else None
                    source_id = raw.get("source_id") if isinstance(raw, dict) else None
                    return IngestApiResult(
                        knowledge_id=str(knowledge_id) if knowledge_id else None,
                        source_id=str(source_id) if source_id else None,
                        http_status=status_code,
                        attempt_count=attempt_count,
                        response_payload={
                            "dry_run": False,
                            "http_status": status_code,
                            "knowledge_id": knowledge_id,
                            "source_id": source_id,
                            "attempt_count": attempt_count,
                            "response": raw,
                        },
                    )
                last_error = IngestApiError(
                    f"ingest API 返回 {status_code}",
                    response_payload={
                        "dry_run": False,
                        "http_status": status_code,
                        "attempt_count": attempt_count,
                        "response": raw,
                    },
                    http_status=status_code,
                    attempt_count=attempt_count,
                )
            except IngestApiError as exc:
                last_error = exc
            except Exception as exc:  # network/client errors
                last_error = IngestApiError(
                    str(exc) or exc.__class__.__name__,
                    response_payload={
                        "dry_run": False,
                        "exception_type": exc.__class__.__name__,
                        "attempt_count": attempt_count,
                    },
                    attempt_count=attempt_count,
                )
            if attempt_index < len(delays):
                self.sleep_fn(delays[attempt_index])
        assert last_error is not None
        raise last_error


def _response_payload(response: Any) -> dict[str, Any] | str:
    try:
        value = response.json()
    except Exception:
        value = getattr(response, "text", "")
    return value if isinstance(value, (dict, str)) else {"response": value}


def ingest_payload_draft(
    repo: DecodeRepository,
    draft: PayloadDraft,
    *,
    dry_run: bool = True,
    client: KnowledgeIngestClient | None = None,
) -> IngestRecord:
    payload = draft.payload or {}
    source_id = ((payload.get("source") or {}).get("id") or "") if isinstance(payload, dict) else ""
    try:
        validate_ingest_payload(payload)
        if dry_run:
            updated = _mark_ingested(repo, draft)
            return repo.save_ingest_record(
                payload_draft_id=draft.id,
                target_system="dry-run",
                target_id=str(draft.id) if draft.id else None,
                status="ingested",
                attempt_count=1,
                response_payload={
                    "dry_run": True,
                    "source_id": source_id,
                    "note": "payload validated by formal ingest path; external ingest API not called",
                    "payload": updated.payload if hasattr(updated, "payload") else payload,
                },
            )
        if client is None:
            raise IngestApiError("真实 ingest 需要 KnowledgeIngestClient")
        result = client.ingest(payload)
        _mark_ingested(repo, draft)
        return repo.save_ingest_record(
            payload_draft_id=draft.id,
            target_system="knowledge-ingest-api",
            target_id=result.knowledge_id,
            status="ingested",
            attempt_count=result.attempt_count,
            response_payload=result.response_payload,
        )
    except (IngestPayloadValidationError, IngestApiError) as exc:
        response_payload = getattr(exc, "response_payload", {}) or {}
        response_payload.setdefault("dry_run", dry_run)
        response_payload.setdefault("source_id", source_id)
        return repo.save_ingest_record(
            payload_draft_id=draft.id,
            target_system="dry-run" if dry_run else "knowledge-ingest-api",
            target_id=None,
            status="failed",
            attempt_count=getattr(exc, "attempt_count", 1),
            response_payload=response_payload,
            error_message=clip_text(str(exc), ERROR_MESSAGE_MAX_CHARS),
        )


def _mark_ingested(repo: DecodeRepository, draft: PayloadDraft) -> PayloadDraft:
    if draft.id is None:
        return draft
    marker = getattr(repo, "mark_payload_draft_ingested", None)
    if marker is None:
        return draft
    return marker(draft.id)
