"""Few-shot 样本入库编排：RiskObservation → FewShotSample → Qdrant。

只处理人工确认为 confirmed / false_positive 的观测，把它转成一条
:class:`FewShotSample`，持久化到 DB，并同步向量到 Qdrant。

入库动作由 :meth:`RiskObservationService.create_feedback` 在 commit 后触发，
本服务全程吞异常（只记日志），保证反馈主流程不被 few-shot 链路拖崩。
"""

from __future__ import annotations

from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.core.logging import get_logger
from app.models.few_shot_sample import FewShotSample
from app.models.risk_observation import RiskObservation, RiskObservationFeedback
from app.services.embedding_provider import EmbeddingProvider
from app.services.few_shot_store import FewShotStore

logger = get_logger("app.services.few_shot_ingestion")

# 仅这两个 feedback_status 视为已确认样本，会入库
CONFIRMED_LABELS = {"confirmed", "false_positive"}

# label → 自然语言结论（当 feedback.comment 缺失时兜底）
LABEL_CONCLUSION_FALLBACK = {
    "confirmed": "经人工复核确认，该风险线索成立，需按规则拦截或补件。",
    "false_positive": "经人工复核判定为误报，相似情形不应触发该风险规则。",
}

CONTROL_STAGE_SCENES = {
    "application": "expense_application",
    "expense_application": "expense_application",
    "pre_application": "expense_application",
    "pre_reimbursement": "expense_reimbursement",
    "reimbursement": "expense_reimbursement",
    "claim": "expense_reimbursement",
    "post_payment": "expense_post_payment",
}


class FewShotIngestionService:
    """把已确认的风险观测沉淀为 few-shot 样本。"""

    def __init__(self, db: Session) -> None:
        self.db = db

    def ingest_observation_feedback(
        self,
        observation: RiskObservation,
        feedback: RiskObservationFeedback,
    ) -> FewShotSample | None:
        """人工确认/误报后调用，写入并同步向量。"""

        label = observation.feedback_status
        if label not in CONFIRMED_LABELS:
            return None
        tenant_id = str(observation.tenant_id or "").strip()
        if not tenant_id:
            logger.warning("few-shot ingestion 缺少 tenant_id observation_id=%s", observation.id)
            return None

        sample_key = f"obs:{observation.id}"
        sample = self.db.scalar(
            select(FewShotSample).where(
                FewShotSample.tenant_id == tenant_id,
                FewShotSample.sample_key == sample_key,
            )
        )

        domain = self._extract_domain(observation)
        scene = self._extract_scene(observation)
        policy_ref, rule_version = self._extract_rule_identity(observation)
        case_text = self._build_case_text(observation)
        conclusion_text = self._build_conclusion_text(observation, feedback, label)
        payload = self._build_payload(observation, feedback, label)

        if sample is None:
            sample = FewShotSample(
                tenant_id=tenant_id,
                sample_key=sample_key,
                source_observation_id=observation.id,
                scene=scene,
                policy_ref=policy_ref,
                rule_version=rule_version,
                domain=domain,
                risk_type=observation.risk_type or "",
                risk_level=observation.risk_level or "",
                label=label,
                case_text=case_text,
                conclusion_text=conclusion_text,
                payload_json=payload,
                status="active",
            )
            self.db.add(sample)
        else:
            sample.tenant_id = tenant_id
            sample.label = label
            sample.scene = scene
            sample.policy_ref = policy_ref
            sample.rule_version = rule_version
            sample.domain = domain
            sample.risk_type = observation.risk_type or ""
            sample.risk_level = observation.risk_level or ""
            sample.case_text = case_text
            sample.conclusion_text = conclusion_text
            sample.payload_json = payload
            sample.status = "active"
        try:
            self.db.commit()
            self.db.refresh(sample)
        except Exception:
            logger.exception("few-shot 样本持久化失败 observation_id=%s", observation.id)
            self.db.rollback()
            return None

        vector_id = self._store().upsert(sample)
        if vector_id:
            sample.vector_id = vector_id
            try:
                self.db.commit()
            except Exception:
                logger.warning("few-shot vector_id 回写失败 sample_id=%s", sample.id)
        return sample

    def retract_observation(self, observation_id: str, *, tenant_id: str) -> bool:
        """观测被撤销时删掉对应样本及其向量。"""

        tenant = str(tenant_id or "").strip()
        if not tenant:
            raise ValueError("tenant_id is required")
        sample = self.db.scalar(
            select(FewShotSample).where(
                FewShotSample.tenant_id == tenant,
                FewShotSample.source_observation_id == observation_id,
            )
        )
        if sample is None:
            return False
        if sample.vector_id:
            self._store().delete_by_vector_id(sample.vector_id)
        try:
            self.db.delete(sample)
            self.db.commit()
            return True
        except Exception:
            logger.exception("few-shot 样本删除失败 observation_id=%s", observation_id)
            self.db.rollback()
            return False

    def _store(self) -> FewShotStore:
        provider = EmbeddingProvider.from_settings(self.db)
        return FewShotStore(provider)

    def _extract_domain(self, observation: RiskObservation) -> str:
        ontology = observation.ontology_json or {}
        return str(ontology.get("domain") or "")

    def _extract_scene(self, observation: RiskObservation) -> str:
        stage = str(observation.control_stage or "").strip().lower()
        if stage in CONTROL_STAGE_SCENES:
            return CONTROL_STAGE_SCENES[stage]
        return stage or "risk_rule_generation"

    def _extract_rule_identity(self, observation: RiskObservation) -> tuple[str, str]:
        trace = observation.decision_trace_json or {}
        policy_ref = _text(
            trace.get("policy_ref") or trace.get("rule_code") or trace.get("policy_code")
        )
        if not policy_ref:
            for value in observation.policy_refs_json or []:
                if isinstance(value, dict):
                    policy_ref = _text(
                        value.get("policy_ref") or value.get("rule_code") or value.get("code")
                    )
                else:
                    policy_ref = _text(value)
                if policy_ref:
                    break
        rule_version = _text(
            trace.get("rule_version")
            or trace.get("policy_version")
            or observation.algorithm_version
        )
        return policy_ref, rule_version

    def _build_case_text(self, observation: RiskObservation) -> str:
        parts = [
            observation.title or "",
            observation.description or "",
            observation.risk_signal or "",
            observation.risk_type or "",
        ]
        ontology = observation.ontology_json or {}
        scenario = ontology.get("scenario")
        if scenario:
            parts.append(f"场景:{scenario}")
        risk_signals = ontology.get("risk_signals")
        if isinstance(risk_signals, list) and risk_signals:
            parts.append("信号:" + "|".join(str(s) for s in risk_signals))
        return "\n".join(part for part in parts if part).strip()

    def _build_conclusion_text(
        self,
        observation: RiskObservation,
        feedback: RiskObservationFeedback,
        label: str,
    ) -> str:
        comment = (feedback.comment or "").strip()
        if comment:
            return f"[{label}] {comment}"
        return LABEL_CONCLUSION_FALLBACK.get(label, label)

    def _build_payload(
        self,
        observation: RiskObservation,
        feedback: RiskObservationFeedback,
        label: str,
    ) -> dict[str, Any]:
        return {
            "tenant_id": observation.tenant_id,
            "scene": self._extract_scene(observation),
            "label": label,
            "risk_type": observation.risk_type,
            "risk_signal": observation.risk_signal,
            "risk_level": observation.risk_level,
            "feedback_type": feedback.feedback_type,
            "feedback_comment": feedback.comment or "",
            "feedback_actor": feedback.actor or "",
            "ontology": observation.ontology_json or {},
            "policy_refs": observation.policy_refs_json or [],
            "policy_ref": self._extract_rule_identity(observation)[0],
            "rule_version": self._extract_rule_identity(observation)[1],
            "evidence": observation.evidence_json or [],
            "subject_label": observation.subject_label or "",
            "claim_no": observation.claim_no or "",
        }


def _text(value: Any) -> str:
    return str(value or "").strip()
