"""市場情報平台 seed 與正式 DB 的只讀差異探針。

本模組只查詢 market_platforms 既有 rows，不使用 DatabaseManager、不建立 ORM session、
不開 explicit transaction、不寫入、不 commit。
"""

import json

from sqlalchemy import bindparam, create_engine, text


def _normalize_policy(value):
    """把 seed policy 與 DB text policy 正規化成可比較字串。"""
    if value in (None, ""):
        return ""
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except json.JSONDecodeError:
            return value.strip()
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def _build_planned_seed_diffs(seed_rows):
    return [
        {
            "code": seed["code"],
            "exists": False,
            "expected_enabled": bool(seed.get("enabled", False)),
            "actual_enabled": None,
            "name_matches": False,
            "base_url_matches": False,
            "policy_matches": False,
            "diff_status": "planned_no_db_connection",
        }
        for seed in seed_rows
    ]


def _build_seed_diff(seed, actual_row):
    if not actual_row:
        return {
            "code": seed["code"],
            "exists": False,
            "expected_enabled": bool(seed.get("enabled", False)),
            "actual_enabled": None,
            "name_matches": False,
            "base_url_matches": False,
            "policy_matches": False,
            "diff_status": "missing",
        }

    expected_policy = _normalize_policy(seed.get("crawl_policy_json"))
    actual_policy = _normalize_policy(actual_row.get("crawl_policy_json"))
    name_matches = (actual_row.get("name") or "") == (seed.get("name") or "")
    base_url_matches = (actual_row.get("base_url") or "") == (seed.get("base_url") or "")
    expected_enabled = bool(seed.get("enabled", False))
    actual_enabled = bool(actual_row.get("enabled", False))
    policy_matches = expected_policy == actual_policy
    diff_status = (
        "matches_expected"
        if name_matches and base_url_matches and policy_matches and expected_enabled == actual_enabled
        else "differs_from_expected"
    )
    return {
        "code": seed["code"],
        "exists": True,
        "expected_enabled": expected_enabled,
        "actual_enabled": actual_enabled,
        "name_matches": name_matches,
        "base_url_matches": base_url_matches,
        "policy_matches": policy_matches,
        "diff_status": diff_status,
    }


def _query_platform_rows(conn, expected_codes):
    if not expected_codes:
        return {}

    rows = conn.execute(
        text(
            """
            SELECT code, name, base_url, enabled, crawl_policy_json
            FROM market_platforms
            WHERE code IN :platform_codes
            ORDER BY code
            """
        ).bindparams(bindparam("platform_codes", expanding=True)),
        {"platform_codes": tuple(expected_codes)},
    ).fetchall()
    return {
        row._mapping["code"]: dict(row._mapping)
        for row in rows
    }


def build_platform_seed_db_diff_plan(
    seed_plan,
    *,
    execute_requested=False,
    database_url=None,
    database_type=None,
    engine=None,
):
    """建立平台 seed DB 差異探針結果；預設只回 planned，不連 DB。"""
    seed_rows = list(seed_plan.get("seeds", []))
    expected_codes = [seed["code"] for seed in seed_rows]

    if not execute_requested:
        return {
            "mode": "platform_seed_db_diff_planned",
            "execute_requested": False,
            "read_only_query_executed": False,
            "database_connection_opened": False,
            "database_session_created": False,
            "explicit_transaction_opened": False,
            "database_write_executed": False,
            "database_commit_executed": False,
            "migration_executed": False,
            "seed_write_executed": False,
            "expected_seed_count": len(seed_rows),
            "existing_seed_count": 0,
            "expected_codes": expected_codes,
            "existing_codes": [],
            "missing_codes": expected_codes,
            "changed_codes": [],
            "matching_codes": [],
            "seed_rows_ready": False,
            "seed_diffs": _build_planned_seed_diffs(seed_rows),
            "blocked_reasons": [
                "execute_false_planned_only",
                "seed_db_diff_not_loaded",
                "seed_write_still_blocked",
            ],
        }

    from config import DATABASE_PATH, DATABASE_TYPE

    effective_database_type = (database_type or DATABASE_TYPE or "").lower()
    effective_database_url = database_url or DATABASE_PATH
    created_engine = False
    connection_opened = False

    try:
        if engine is None:
            connect_args = {}
            if effective_database_type == "postgresql":
                connect_args = {
                    "connect_timeout": 8,
                    "options": "-c statement_timeout=15000",
                }
            engine = create_engine(
                effective_database_url,
                isolation_level="AUTOCOMMIT",
                pool_pre_ping=True,
                connect_args=connect_args,
            )
            created_engine = True

        with engine.connect() as conn:
            connection_opened = True
            existing_by_code = _query_platform_rows(conn, expected_codes)

        seed_diffs = [
            _build_seed_diff(seed, existing_by_code.get(seed["code"]))
            for seed in seed_rows
        ]
        missing_codes = [
            item["code"] for item in seed_diffs
            if item["diff_status"] == "missing"
        ]
        changed_codes = [
            item["code"] for item in seed_diffs
            if item["diff_status"] == "differs_from_expected"
        ]
        matching_codes = [
            item["code"] for item in seed_diffs
            if item["diff_status"] == "matches_expected"
        ]
        blocked_reasons = ["seed_write_still_blocked"]
        if missing_codes:
            blocked_reasons.insert(0, "seed_rows_missing")
        if changed_codes:
            blocked_reasons.insert(0, "seed_rows_differ")

        return {
            "mode": "platform_seed_db_diff_read_only",
            "execute_requested": True,
            "read_only_query_executed": True,
            "database_connection_opened": connection_opened,
            "database_session_created": False,
            "explicit_transaction_opened": False,
            "database_write_executed": False,
            "database_commit_executed": False,
            "migration_executed": False,
            "seed_write_executed": False,
            "expected_seed_count": len(seed_rows),
            "existing_seed_count": len(existing_by_code),
            "expected_codes": expected_codes,
            "existing_codes": sorted(existing_by_code),
            "missing_codes": missing_codes,
            "changed_codes": changed_codes,
            "matching_codes": matching_codes,
            "seed_rows_ready": not missing_codes and not changed_codes,
            "seed_diffs": seed_diffs,
            "blocked_reasons": blocked_reasons,
        }
    except Exception as exc:
        return {
            "mode": "platform_seed_db_diff_error",
            "execute_requested": True,
            "read_only_query_executed": False,
            "database_connection_opened": connection_opened,
            "database_session_created": False,
            "explicit_transaction_opened": False,
            "database_write_executed": False,
            "database_commit_executed": False,
            "migration_executed": False,
            "seed_write_executed": False,
            "expected_seed_count": len(seed_rows),
            "existing_seed_count": 0,
            "expected_codes": expected_codes,
            "existing_codes": [],
            "missing_codes": expected_codes,
            "changed_codes": [],
            "matching_codes": [],
            "seed_rows_ready": False,
            "seed_diffs": _build_planned_seed_diffs(seed_rows),
            "blocked_reasons": [
                "platform_seed_db_diff_error",
                "seed_write_still_blocked",
            ],
            "error_message": str(exc),
        }
    finally:
        if created_engine:
            engine.dispose()
