"""候選審核 queue AI summary output receipt 預覽。

本模組只驗收人工 Ollama 摘要輸出是否符合 run package schema 與 evidence contract；
不呼叫 LLM、不派送 Telegram、不讀 approval token、不執行 CLI、不寫檔、
不寫 DB、不更新 review_state、不 commit、不掛 scheduler。
"""


FORBIDDEN_TOKEN_KEYWORDS = (
    "approval_token",
    "approval-token",
    "market_intel_queue_write_approval",
)
SAFE_TOKEN_METADATA_KEYS = {
    "approval_token_present",
    "approval_token_valid",
    "approval_token_secret_configured",
}
SAFE_APPROVAL_ENV_VAR = "MARKET_INTEL_QUEUE_WRITE_APPROVAL"
TARGET_TABLE = "market_alert_review_queue"


def _as_dict(value):
    return value if isinstance(value, dict) else {}


def _as_list(value):
    if value is None:
        return []
    if isinstance(value, (list, tuple, set)):
        return list(value)
    return [value]


def _contains_forbidden_token_key(value):
    if isinstance(value, dict):
        for key, nested in value.items():
            normalized_key = str(key).lower()
            if normalized_key in SAFE_TOKEN_METADATA_KEYS and isinstance(nested, bool):
                continue
            if normalized_key == "approval_env_var" and nested == SAFE_APPROVAL_ENV_VAR:
                continue
            if any(token_key in normalized_key for token_key in FORBIDDEN_TOKEN_KEYWORDS):
                return True
            if _contains_forbidden_token_key(nested):
                return True
    elif isinstance(value, list):
        return any(_contains_forbidden_token_key(item) for item in value)
    return False


def _safe_text(value, limit=300):
    if value is None:
        return None
    text = str(value).strip()
    return text[:limit] if text else None


def _side_effects_clear(*payloads):
    blocked_keys = (
        "api_executes_cli",
        "api_executes_llm",
        "api_reads_approval_token",
        "api_writes_file",
        "api_writes_database",
        "api_updates_review_state",
        "run_package_file_written",
        "summary_file_written",
        "summary_record_written",
        "summary_manifest_written",
        "summary_receipt_file_written",
        "database_write_executed",
        "database_commit_executed",
        "review_state_update_executed",
        "llm_call_executed",
        "ollama_call_executed",
        "gemini_call_executed",
        "telegram_dispatched",
        "ai_summary_generated",
        "external_network_executed",
        "scheduler_attached",
        "writes_executed",
        "would_write_database",
    )
    return all(
        not _as_dict(payload).get(key)
        for payload in payloads
        for key in blocked_keys
    )


def _operator_summary(operator_evidence):
    operator_evidence = _as_dict(operator_evidence)
    output = operator_evidence.get("ai_summary_output") or operator_evidence.get(
        "summary_output"
    )
    return {
        "provided_keys": sorted(operator_evidence.keys()),
        "operator_confirmed_ai_summary_output_receipt": bool(
            operator_evidence.get("operator_confirmed_ai_summary_output_receipt")
            or operator_evidence.get("operator_confirmed_summary_output_receipt")
        ),
        "operator_confirmed_manual_output_only": bool(
            operator_evidence.get("operator_confirmed_manual_output_only")
            or operator_evidence.get("operator_confirmed_manual_ollama_summary_run")
        ),
        "operator_confirmed_ollama_first": bool(
            operator_evidence.get("operator_confirmed_ollama_first")
            or operator_evidence.get("operator_confirmed_ollama_cascade")
        ),
        "operator_confirmed_gemini_backup_only": bool(
            operator_evidence.get("operator_confirmed_gemini_backup_only")
            or operator_evidence.get("operator_confirmed_gemini_fallback_only")
        ),
        "operator_confirmed_no_api_llm_call": bool(
            operator_evidence.get("operator_confirmed_no_api_llm_call")
            or operator_evidence.get("operator_confirmed_no_llm_call")
        ),
        "operator_confirmed_no_telegram_dispatch": bool(
            operator_evidence.get("operator_confirmed_no_telegram_dispatch")
        ),
        "operator_confirmed_no_scheduler_attach": bool(
            operator_evidence.get("operator_confirmed_no_scheduler_attach")
        ),
        "operator_confirmed_no_api_db_write": bool(
            operator_evidence.get("operator_confirmed_no_api_db_write")
        ),
        "ai_summary_output_artifact_path": _safe_text(
            operator_evidence.get("ai_summary_output_artifact_path")
            or operator_evidence.get("summary_output_artifact_path")
        ),
        "ai_summary_receipt_artifact_path": _safe_text(
            operator_evidence.get("ai_summary_receipt_artifact_path")
            or operator_evidence.get("summary_receipt_artifact_path")
        ),
        "manual_ai_summary_output_provided": isinstance(output, dict),
        "approval_token_submitted_to_api": _contains_forbidden_token_key(
            operator_evidence
        ),
    }


def _run_package_payload(ai_summary_run_package):
    package = _as_dict(ai_summary_run_package)
    prompt = _as_dict(package.get("prompt_contract"))
    input_contract = _as_dict(prompt.get("input_contract"))
    output_schema = _as_dict(package.get("summary_output_schema"))
    return {
        "provided": bool(package),
        "mode": package.get("mode"),
        "ai_summary_run_package_ready": bool(
            package.get("ai_summary_run_package_ready")
        ),
        "ready_for_manual_ollama_summary_run": bool(
            package.get("ready_for_manual_ollama_summary_run")
        ),
        "model_route_policy": _as_dict(package.get("model_route_policy")),
        "prompt_contract_version": prompt.get("contract_version"),
        "summary_output_schema": output_schema,
        "required_fields": _as_list(output_schema.get("required_fields")),
        "expected_dedupe_keys": _as_list(package.get("expected_dedupe_keys")),
        "summary_section_keys": _as_list(package.get("summary_section_keys")),
        "artifact_paths": _as_dict(input_contract.get("artifact_paths")),
        "safe_boundaries": _as_list(package.get("safe_boundaries")),
        "blocked_reasons": _as_list(package.get("blocked_reasons")),
        "read_only_query_executed": bool(package.get("read_only_query_executed")),
        "database_connection_opened": bool(package.get("database_connection_opened")),
        "database_write_executed": bool(package.get("database_write_executed")),
        "database_commit_executed": bool(package.get("database_commit_executed")),
        "review_state_update_executed": bool(
            package.get("review_state_update_executed")
        ),
        "llm_call_executed": bool(package.get("llm_call_executed")),
        "ollama_call_executed": bool(package.get("ollama_call_executed")),
        "gemini_call_executed": bool(package.get("gemini_call_executed")),
        "telegram_dispatched": bool(package.get("telegram_dispatched")),
        "ai_summary_generated": bool(package.get("ai_summary_generated")),
        "scheduler_attached": bool(package.get("scheduler_attached")),
    }


def _summary_output(operator_evidence):
    output = _as_dict(operator_evidence).get("ai_summary_output") or _as_dict(
        operator_evidence
    ).get("summary_output")
    return _as_dict(output)


def _required_field_status(*, output, required_fields):
    missing = [field for field in required_fields if field not in output]
    empty_allowed = {"risk_flags"}
    empty = [
        field
        for field in required_fields
        if field in output
        and field not in empty_allowed
        and output.get(field) in (None, "", [], {})
    ]
    return missing, empty


def _output_shape_valid(output):
    if not output:
        return False
    if not isinstance(output.get("headline"), str):
        return False
    if not isinstance(output.get("executive_summary"), str):
        return False
    for key in ("key_findings", "risk_flags", "recommended_actions", "evidence_refs"):
        if not isinstance(output.get(key), list):
            return False
    return isinstance(output.get("model_route"), dict)


def _allowed_evidence_refs(package):
    artifact_paths = package["artifact_paths"]
    return sorted(
        set(str(item) for item in package["expected_dedupe_keys"])
        | set(str(item) for item in package["summary_section_keys"])
        | set(str(key) for key in artifact_paths.keys())
        | set(str(value) for value in artifact_paths.values() if value)
    )


def _evidence_refs_grounded(*, output, package):
    refs = [str(item) for item in _as_list(output.get("evidence_refs")) if item]
    allowed = set(_allowed_evidence_refs(package))
    return bool(refs) and all(ref in allowed for ref in refs), refs


def _model_route_accepted(*, output, package):
    model_route = _as_dict(output.get("model_route"))
    route_policy = package["model_route_policy"]
    cascade = _as_list(route_policy.get("primary_cascade"))
    allowed_hosts = {str(_as_dict(node).get("host")) for node in cascade}
    provider = model_route.get("provider")
    host = model_route.get("host")
    fallback_reason = _safe_text(model_route.get("fallback_reason"))
    failed_hosts = set(str(host) for host in _as_list(model_route.get("ollama_failed_hosts")))
    if route_policy.get("primary_policy") != "ollama_first":
        return False
    if route_policy.get("app_host_forbidden_as_ollama_node") == host:
        return False
    if provider == "ollama":
        return bool(host in allowed_hosts)
    if provider == "gemini_fallback":
        return bool(
            route_policy.get("fallback_policy")
            == "gemini_backup_only_after_ollama_cascade_failure"
            and fallback_reason
            and len(failed_hosts & allowed_hosts) == len(allowed_hosts)
        )
    return False


def _receipt_gates(*, package, output, operator):
    missing_fields, empty_fields = _required_field_status(
        output=output,
        required_fields=package["required_fields"],
    )
    grounded, refs = _evidence_refs_grounded(output=output, package=package)
    route_accepted = _model_route_accepted(output=output, package=package)
    return [
        {
            "key": "ai_summary_run_package_ready",
            "label": "AI summary run package 必須已通過",
            "passed": bool(
                package["provided"]
                and package["mode"]
                == "candidate_queue_review_ai_summary_run_package_preview"
                and package["ai_summary_run_package_ready"]
                and package["ready_for_manual_ollama_summary_run"]
            ),
        },
        {
            "key": "manual_ai_summary_output_provided",
            "label": "必須提供人工 Ollama 摘要輸出 JSON",
            "passed": bool(output and operator["manual_ai_summary_output_provided"]),
        },
        {
            "key": "summary_output_required_fields_present",
            "label": "摘要輸出必須包含 schema 必要欄位",
            "passed": bool(package["required_fields"] and not missing_fields and not empty_fields),
        },
        {
            "key": "summary_output_shape_valid",
            "label": "摘要輸出欄位型別必須符合 contract",
            "passed": _output_shape_valid(output),
        },
        {
            "key": "summary_output_evidence_refs_grounded",
            "label": "摘要 evidence_refs 必須對應 dedupe key、section 或 artifact",
            "passed": grounded,
        },
        {
            "key": "summary_output_model_route_accepted",
            "label": "摘要 model_route 必須符合 Ollama-first / Gemini fallback-only",
            "passed": route_accepted,
        },
        {
            "key": "operator_confirmed_ai_summary_output_receipt",
            "label": "操作員確認本步只驗收人工摘要輸出",
            "passed": bool(
                operator["operator_confirmed_ai_summary_output_receipt"]
                and operator["operator_confirmed_manual_output_only"]
                and operator["operator_confirmed_ollama_first"]
                and operator["operator_confirmed_gemini_backup_only"]
                and operator["operator_confirmed_no_api_llm_call"]
                and operator["operator_confirmed_no_telegram_dispatch"]
                and operator["operator_confirmed_no_api_db_write"]
                and operator["operator_confirmed_no_scheduler_attach"]
            ),
        },
        {
            "key": "ai_summary_output_receipt_artifacts_recorded",
            "label": "summary output 與 receipt artifact path 必須由操作員提供",
            "passed": bool(
                operator["ai_summary_output_artifact_path"]
                and operator["ai_summary_receipt_artifact_path"]
            ),
        },
        {
            "key": "ai_summary_output_receipt_no_approval_token_submitted_to_api",
            "label": "payload 不得包含一次性 approval token key",
            "passed": not operator["approval_token_submitted_to_api"],
        },
        {
            "key": "ai_summary_output_receipt_has_no_side_effects",
            "label": "receipt 不得呼叫 LLM、派送 Telegram、寫檔、寫 DB 或掛 scheduler",
            "passed": _side_effects_clear(package),
        },
    ], {
        "missing_fields": missing_fields,
        "empty_fields": empty_fields,
        "evidence_refs": refs,
        "allowed_evidence_refs": _allowed_evidence_refs(package),
        "model_route_accepted": route_accepted,
    }


def build_candidate_queue_review_ai_summary_output_receipt(
    *,
    ai_summary_run_package,
    operator_evidence=None,
    execute_requested=False,
):
    """建立人工 AI summary output receipt 預覽；不呼叫模型或執行副作用。"""
    package = _run_package_payload(ai_summary_run_package)
    operator = _operator_summary(operator_evidence)
    output = _summary_output(operator_evidence)
    gates, validation = _receipt_gates(
        package=package,
        output=output,
        operator=operator,
    )
    blocked_reasons = [gate["key"] for gate in gates if not gate["passed"]]
    receipt_ready = bool(not blocked_reasons)

    return {
        "mode": "candidate_queue_review_ai_summary_output_receipt_preview",
        "target_table": TARGET_TABLE,
        "target_operation": "review_manual_ollama_ai_summary_output",
        "execute_requested": bool(execute_requested),
        "ai_summary_output_receipt_ready": receipt_ready,
        "ready_for_summary_persistence_review": receipt_ready,
        "ready_for_next_manual_phase": receipt_ready,
        "manual_ai_summary_output_provided": operator[
            "manual_ai_summary_output_provided"
        ],
        "summary_output_schema_valid": bool(
            output and not validation["missing_fields"] and not validation["empty_fields"]
        ),
        "summary_output_evidence_refs_grounded": bool(
            validation["evidence_refs"]
            and all(
                ref in set(validation["allowed_evidence_refs"])
                for ref in validation["evidence_refs"]
            )
        ),
        "summary_output_model_route_accepted": validation["model_route_accepted"],
        "ready_for_ai_summary_generation": False,
        "ready_for_llm_call": False,
        "ready_for_telegram_dispatch": False,
        "ready_for_api_review_state_update": False,
        "ready_for_api_database_write": False,
        "ready_for_scheduler_attach": False,
        "api_executes_cli": False,
        "api_executes_llm": False,
        "api_reads_approval_token": False,
        "api_writes_file": False,
        "api_writes_database": False,
        "api_updates_review_state": False,
        "approval_record_written": False,
        "decision_record_written": False,
        "run_package_file_written": False,
        "summary_file_written": False,
        "summary_record_written": False,
        "summary_manifest_written": False,
        "summary_receipt_file_written": False,
        "ai_summary_generated": False,
        "llm_call_executed": False,
        "ollama_call_executed": False,
        "gemini_call_executed": False,
        "telegram_dispatched": False,
        "review_state_update_executed": False,
        "read_only_query_executed": package["read_only_query_executed"],
        "database_connection_opened": package["database_connection_opened"],
        "database_session_created": False,
        "explicit_transaction_opened": False,
        "transaction_opened": False,
        "transaction_committed": False,
        "database_write_executed": False,
        "database_commit_executed": False,
        "database_rollback_executed": False,
        "external_network_executed": False,
        "scheduler_attached": False,
        "writes_executed": False,
        "would_write_database": False,
        "expected_dedupe_keys": package["expected_dedupe_keys"],
        "summary_section_keys": package["summary_section_keys"],
        "summary_output_required_fields": package["required_fields"],
        "summary_output_validation": validation,
        "model_route_policy": package["model_route_policy"],
        "operator_ai_summary_output_receipt": operator,
        "blocked_reasons": blocked_reasons,
        "gates": gates,
        "next_operator_steps": [
            "確認人工摘要 output JSON 已符合 schema 與 evidence_refs",
            "若要持久化 summary 或派送 Telegram，必須另開 persistence/dispatch gate",
            "保留 Ollama host 或 Gemini fallback reason 供後續稽核",
            "本 receipt 不寫檔、不寫 DB、不呼叫模型、不發 Telegram",
        ],
        "safe_boundaries": [
            "do_not_call_llm_from_ai_summary_output_receipt",
            "do_not_dispatch_telegram_from_ai_summary_output_receipt",
            "do_not_write_summary_receipt_file_from_api",
            "do_not_write_summary_file_from_api",
            "do_not_insert_summary_record_from_api",
            "do_not_update_review_state_from_ai_summary_output_receipt",
            "do_not_read_approval_token_from_ai_summary_output_receipt",
            "do_not_execute_cli_from_ai_summary_output_receipt",
            "do_not_attach_scheduler_from_ai_summary_output_receipt",
            "manual_ai_summary_output_only",
            "ollama_cascade_must_use_gcp_a_then_gcp_b_then_111",
            "gemini_backup_only_after_ollama_cascade_failure",
            "host_188_forbidden_as_ollama_node",
            "ai_summary_output_receipt_preview_only",
            "no_remove_orphans",
            "no_momo_db_lifecycle_change",
        ],
    }
