# -*- coding: utf-8 -*-
"""Export 60-day member visit detail with WBI/NCI scores."""

from __future__ import annotations

import argparse
import csv
import math
import os
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT not in sys.path:
    sys.path.insert(0, ROOT)

from config.settings import AppConfig
from database.connection import DatabaseConnection
from database.operations import DatabaseOperations


FIELDS = [
    "site_id",
    "member_id",
    "member_nickname",
    "visit_time",
    "consume_amount",
    "sv_balance",
    "assistant_nicknames",
    "wbi_score",
    "nci_score",
]


def _as_int(v: Any) -> Optional[int]:
    if v is None or str(v).strip() == "":
        return None
    return int(v)


def _as_float(v: Any, default: float = 0.0) -> float:
    if v is None or str(v).strip() == "":
        return default
    return float(v)


def _resolve_site_id(config: AppConfig, db: DatabaseOperations, cli_site_id: Optional[int]) -> int:
    if cli_site_id is not None:
        return int(cli_site_id)

    from_cfg = _as_int(config.get("app.store_id")) or _as_int(config.get("app.default_site_id"))
    if from_cfg is not None:
        return from_cfg

    rows = db.query(
        """
        SELECT site_id
        FROM dwd.dwd_settlement_head
        WHERE site_id IS NOT NULL
        GROUP BY site_id
        ORDER BY COUNT(*) DESC
        LIMIT 1
        """
    )
    if rows:
        return int(dict(rows[0])["site_id"])

    raise RuntimeError("Unable to resolve site_id; pass --site-id explicitly.")


def _visit_condition_sql() -> str:
    return """
        (
            s.settle_type = 1
            OR (
                s.settle_type = 3
                AND EXISTS (
                    SELECT 1
                    FROM dwd.dwd_assistant_service_log asl
                    JOIN dws.cfg_skill_type st
                        ON asl.skill_id = st.skill_id
                       AND st.course_type_code = 'BONUS'
                       AND st.is_active = TRUE
                    WHERE asl.order_settle_id = s.order_settle_id
                      AND asl.site_id = s.site_id
                      AND asl.tenant_member_id = s.member_id
                      AND asl.is_delete = 0
                )
            )
        )
    """


def _fetch_visit_rows_base(
    db: DatabaseOperations,
    site_id: int,
    start_time: datetime,
    end_time: datetime,
) -> List[Dict[str, Any]]:
    sql = f"""
    WITH visit_raw AS (
        SELECT
            s.site_id,
            COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id) AS member_id,
            s.order_settle_id,
            s.pay_time AS visit_time,
            COALESCE(s.consume_money, 0) AS consume_amount
        FROM dwd.dwd_settlement_head s
        LEFT JOIN dwd.dim_member_card_account mca
            ON s.member_card_account_id = mca.member_card_id
           AND mca.scd2_is_current = 1
           AND mca.register_site_id = s.site_id
        WHERE s.site_id = %s
          AND s.pay_time >= %s
          AND s.pay_time < %s
          AND {_visit_condition_sql()}
          AND COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id) > 0
    ),
    assistant_agg AS (
        SELECT
            asl.order_settle_id,
            STRING_AGG(DISTINCT NULLIF(asl.nickname, ''), '?' ORDER BY NULLIF(asl.nickname, '')) AS assistant_nicknames
        FROM dwd.dwd_assistant_service_log asl
        WHERE asl.site_id = %s
          AND asl.is_delete = 0
        GROUP BY asl.order_settle_id
    ),
    member_balance AS (
        SELECT
            mca.register_site_id AS site_id,
            mca.tenant_member_id AS member_id,
            SUM(
                CASE
                    WHEN mca.card_type_id = 2793249295533893 THEN COALESCE(mca.balance, 0)
                    ELSE 0
                END
            ) AS sv_balance
        FROM dwd.dim_member_card_account mca
        WHERE mca.register_site_id = %s
          AND mca.scd2_is_current = 1
        GROUP BY mca.register_site_id, mca.tenant_member_id
    ),
    member_name AS (
        SELECT member_id, nickname
        FROM dwd.dim_member
        WHERE register_site_id = %s
          AND scd2_is_current = 1
    )
    SELECT
        vr.site_id,
        vr.member_id,
        COALESCE(mn.nickname, CONCAT('member_', vr.member_id::text)) AS member_nickname,
        vr.visit_time,
        ROUND(vr.consume_amount::numeric, 2) AS consume_amount,
        ROUND(COALESCE(mb.sv_balance, 0)::numeric, 2) AS sv_balance,
        aa.assistant_nicknames
    FROM visit_raw vr
    LEFT JOIN assistant_agg aa
        ON aa.order_settle_id = vr.order_settle_id
    LEFT JOIN member_balance mb
        ON mb.site_id = vr.site_id
       AND mb.member_id = vr.member_id
    LEFT JOIN member_name mn
        ON mn.member_id = vr.member_id
    ORDER BY vr.visit_time DESC, vr.order_settle_id DESC
    """
    rows = db.query(sql, (site_id, start_time, end_time, site_id, site_id, site_id))
    return [dict(r) for r in (rows or [])]


def _fetch_current_score_maps(
    db: DatabaseOperations,
    site_id: int,
) -> Tuple[Dict[int, float], Dict[int, float]]:
    wbi_rows = db.query(
        """
        SELECT member_id, display_score AS wbi_score
        FROM dws.dws_member_winback_index
        WHERE site_id = %s
        """,
        (site_id,),
    )
    nci_rows = db.query(
        """
        SELECT member_id, display_score AS nci_score
        FROM dws.dws_member_newconv_index
        WHERE site_id = %s
        """,
        (site_id,),
    )
    wbi_map = {
        int(dict(r)["member_id"]): round(float(dict(r)["wbi_score"]), 2)
        for r in (wbi_rows or [])
        if dict(r).get("wbi_score") is not None
    }
    nci_map = {
        int(dict(r)["member_id"]): round(float(dict(r)["nci_score"]), 2)
        for r in (nci_rows or [])
        if dict(r).get("nci_score") is not None
    }
    return wbi_map, nci_map


def _load_wbi_params(db: DatabaseOperations) -> Dict[str, float]:
    sql = """
    SELECT param_name, param_value
    FROM (
        SELECT
            param_name,
            param_value,
            ROW_NUMBER() OVER (
                PARTITION BY param_name
                ORDER BY effective_from DESC, updated_at DESC, created_at DESC
            ) AS rn
        FROM dws.cfg_index_parameters
        WHERE index_type = 'WBI'
          AND effective_from <= CURRENT_DATE
    ) t
    WHERE rn = 1
    """
    rows = db.query(sql)
    params: Dict[str, float] = {}
    for row in (rows or []):
        d = dict(row)
        params[str(d["param_name"])] = float(d["param_value"])
    return params


def _fetch_wbi_member_rows(db: DatabaseOperations, site_id: int) -> Dict[int, Dict[str, Any]]:
    rows = db.query(
        """
        SELECT
            member_id,
            status,
            segment,
            t_v,
            interval_count,
            overdue_old,
            drop_old,
            recharge_old,
            value_old,
            raw_score,
            display_score
        FROM dws.dws_member_winback_index
        WHERE site_id = %s
        """,
        (site_id,),
    )
    result: Dict[int, Dict[str, Any]] = {}
    for row in (rows or []):
        d = dict(row)
        mid = int(d["member_id"])
        result[mid] = d
    return result


def _fetch_member_interval_samples(
    db: DatabaseOperations,
    site_id: int,
    member_ids: List[int],
    base_date: date,
    visit_lookback_days: int,
    recency_days: int,
) -> Dict[int, List[Tuple[float, int]]]:
    if not member_ids:
        return {}
    member_ids_str = ",".join(str(m) for m in member_ids)
    start_date = base_date - timedelta(days=visit_lookback_days)
    sql = f"""
    WITH visit_source AS (
        SELECT
            COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id) AS member_id,
            DATE(s.pay_time) AS visit_date
        FROM dwd.dwd_settlement_head s
        LEFT JOIN dwd.dim_member_card_account mca
            ON s.member_card_account_id = mca.member_card_id
           AND mca.scd2_is_current = 1
           AND mca.register_site_id = s.site_id
        WHERE s.site_id = %s
          AND s.pay_time >= %s
          AND s.pay_time < %s + INTERVAL '1 day'
          AND {_visit_condition_sql()}
          AND COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id) IN ({member_ids_str})
    ),
    visit_dedup AS (
        SELECT member_id, visit_date
        FROM visit_source
        GROUP BY member_id, visit_date
    )
    SELECT member_id, visit_date
    FROM visit_dedup
    ORDER BY member_id, visit_date
    """
    rows = db.query(sql, (site_id, start_date, base_date))
    member_dates: Dict[int, List[date]] = {}
    for row in (rows or []):
        d = dict(row)
        mid = int(d["member_id"])
        vdt = d["visit_date"]
        if vdt is None:
            continue
        member_dates.setdefault(mid, []).append(vdt)

    result: Dict[int, List[Tuple[float, int]]] = {}
    for mid, dates in member_dates.items():
        samples: List[Tuple[float, int]] = []
        for i in range(1, len(dates)):
            interval = (dates[i] - dates[i - 1]).days
            interval_capped = float(min(recency_days, interval))
            age_days = max(0, (base_date - dates[i]).days)
            samples.append((interval_capped, age_days))
        result[mid] = samples
    return result


def _weighted_cdf(
    samples: List[Tuple[float, int]],
    t_v: float,
    halflife_days: float,
    blend_min_samples: int = 8,
) -> float:
    if not samples:
        return 0.5
    if halflife_days <= 0:
        p_eq = sum(1.0 for x, _ in samples if x <= t_v) / len(samples)
        return p_eq

    ln2 = math.log(2.0)
    weights: List[float] = []
    indicators: List[float] = []
    for interval, age_days in samples:
        w = math.exp(-ln2 * float(age_days) / halflife_days)
        weights.append(w)
        indicators.append(1.0 if interval <= t_v else 0.0)

    w_sum = sum(weights)
    if w_sum <= 0:
        p_w = 0.5
    else:
        p_w = sum(w * ind for w, ind in zip(weights, indicators)) / w_sum
    p_eq = sum(indicators) / len(indicators)

    m = len(samples)
    lam = min(1.0, float(m) / float(max(1, blend_min_samples)))
    p = lam * p_w + (1.0 - lam) * p_eq
    return max(0.0, min(1.0, p))


def _calculate_percentiles(scores: List[float], lower: int, upper: int) -> Tuple[float, float]:
    if not scores:
        return 0.0, 0.0
    sorted_scores = sorted(scores)
    n = len(sorted_scores)
    lower_idx = max(0, int(n * lower / 100) - 1)
    upper_idx = min(n - 1, int(n * upper / 100))
    return sorted_scores[lower_idx], sorted_scores[upper_idx]


def _winsorize(value: float, lower: float, upper: float) -> float:
    return min(max(value, lower), upper)


def _normalize_to_display(value: float, min_val: float, max_val: float, compression_mode: str) -> float:
    if compression_mode == "log1p":
        value = math.log1p(value)
        min_val = math.log1p(min_val)
        max_val = math.log1p(max_val)
    elif compression_mode == "asinh":
        value = math.asinh(value)
        min_val = math.asinh(min_val)
        max_val = math.asinh(max_val)

    eps = 1e-6
    rng = max_val - min_val
    if rng < eps:
        return 5.0
    score = 10.0 * (value - min_val) / rng
    return max(0.0, min(10.0, score))


def _compression_mode_from_param(params: Dict[str, float]) -> str:
    mode = int(params.get("compression_mode", 0))
    if mode == 1:
        return "log1p"
    if mode == 2:
        return "asinh"
    return "none"


def _build_wbi_optimized_map(
    db: DatabaseOperations,
    site_id: int,
    base_date: date,
    half_life_days: float,
) -> Dict[int, Optional[float]]:
    params = _load_wbi_params(db)
    w_over = float(params.get("w_over", 2.0))
    w_drop = float(params.get("w_drop", 1.0))
    w_re = float(params.get("w_re", 0.4))
    w_value = float(params.get("w_value", 1.2))
    overdue_alpha = float(params.get("overdue_alpha", 2.0))
    percentile_lower = int(params.get("percentile_lower", 5))
    percentile_upper = int(params.get("percentile_upper", 95))
    recency_days = int(params.get("lookback_days_recency", 60))
    visit_lookback_days = int(params.get("visit_lookback_days", 180))

    member_rows = _fetch_wbi_member_rows(db, site_id)
    member_ids_for_calc = [
        mid
        for mid, row in member_rows.items()
        if row.get("segment") == "OLD" and row.get("raw_score") is not None
    ]
    interval_samples = _fetch_member_interval_samples(
        db=db,
        site_id=site_id,
        member_ids=member_ids_for_calc,
        base_date=base_date,
        visit_lookback_days=visit_lookback_days,
        recency_days=recency_days,
    )

    raw_new_map: Dict[int, float] = {}
    for mid in member_ids_for_calc:
        row = member_rows[mid]
        t_v = _as_float(row.get("t_v"), recency_days)
        overdue_old = _as_float(row.get("overdue_old"))
        drop_old = _as_float(row.get("drop_old"))
        recharge_old = _as_float(row.get("recharge_old"))
        value_old = _as_float(row.get("value_old"))
        raw_old = _as_float(row.get("raw_score"))

        pre_old = (
            w_over * overdue_old
            + w_drop * drop_old
            + w_re * recharge_old
            + w_value * value_old
        )
        if pre_old <= 1e-9:
            suppression = 1.0
        else:
            suppression = max(0.0, min(1.0, raw_old / pre_old))

        p_weighted = _weighted_cdf(
            samples=interval_samples.get(mid, []),
            t_v=t_v,
            halflife_days=half_life_days,
        )
        overdue_new = math.pow(p_weighted, overdue_alpha)
        pre_new = (
            w_over * overdue_new
            + w_drop * drop_old
            + w_re * recharge_old
            + w_value * value_old
        )
        raw_new = max(0.0, pre_new * suppression)
        raw_new_map[mid] = raw_new

    if not raw_new_map:
        return {mid: _as_float(row.get("display_score")) for mid, row in member_rows.items()}

    scores = list(raw_new_map.values())
    q_l, q_u = _calculate_percentiles(scores, percentile_lower, percentile_upper)
    compression_mode = _compression_mode_from_param(params)

    display_new_map: Dict[int, Optional[float]] = {}
    for mid, raw_score in raw_new_map.items():
        clipped = _winsorize(raw_score, q_l, q_u)
        display = _normalize_to_display(clipped, q_l, q_u, compression_mode=compression_mode)
        display_new_map[mid] = round(display, 2)

    # 保留未重新计算的会员（如 STOP_HIGH_BALANCE）的当前展示分数。
    result: Dict[int, Optional[float]] = {}
    for mid, row in member_rows.items():
        if mid in display_new_map:
            result[mid] = display_new_map[mid]
        else:
            current = row.get("display_score")
            result[mid] = None if current is None else round(float(current), 2)
    return result


def _attach_scores(
    base_rows: List[Dict[str, Any]],
    wbi_map: Dict[int, Optional[float]],
    nci_map: Dict[int, float],
) -> List[Dict[str, Any]]:
    result: List[Dict[str, Any]] = []
    for row in base_rows:
        mid = int(row["member_id"])
        new_row = {
            "site_id": row.get("site_id"),
            "member_id": row.get("member_id"),
            "member_nickname": row.get("member_nickname"),
            "visit_time": row.get("visit_time"),
            "consume_amount": row.get("consume_amount"),
            "sv_balance": row.get("sv_balance"),
            "assistant_nicknames": row.get("assistant_nicknames"),
            "wbi_score": wbi_map.get(mid),
            "nci_score": nci_map.get(mid),
        }
        result.append(new_row)
    return result


def _write_csv(rows: List[Dict[str, Any]], out_csv: Path) -> None:
    out_csv.parent.mkdir(parents=True, exist_ok=True)
    with out_csv.open("w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for row in rows:
            writer.writerow({k: row.get(k) for k in FIELDS})


def _write_preview_md(rows: List[Dict[str, Any]], out_md: Path, limit: int = 200) -> None:
    out_md.parent.mkdir(parents=True, exist_ok=True)
    lines = [
        "|" + "|".join(FIELDS) + "|",
        "|" + "|".join(["---"] * len(FIELDS)) + "|",
    ]
    for row in rows[:limit]:
        cells = ["" if row.get(c) is None else str(row.get(c)) for c in FIELDS]
        lines.append("|" + "|".join(cells) + "|")
    out_md.write_text("\n".join(lines), encoding="utf-8-sig")


def _diff_and_write_report(
    current_rows: List[Dict[str, Any]],
    optimized_rows: List[Dict[str, Any]],
    out_md: Path,
) -> None:
    def _to_map(rows: List[Dict[str, Any]]) -> Dict[Tuple[Any, Any, Any], Dict[str, Any]]:
        result: Dict[Tuple[Any, Any, Any], Dict[str, Any]] = {}
        for r in rows:
            key = (r.get("site_id"), r.get("member_id"), r.get("visit_time"))
            result[key] = r
        return result

    cur_map = _to_map(current_rows)
    opt_map = _to_map(optimized_rows)
    cur_keys = set(cur_map.keys())
    opt_keys = set(opt_map.keys())
    common_keys = sorted(cur_keys & opt_keys)

    changed_rows = 0
    changed_wbi_rows = 0
    changed_nci_rows = 0
    changed_member_ids = set()
    member_wbi_deltas: Dict[int, List[float]] = {}

    for k in common_keys:
        c = cur_map[k]
        o = opt_map[k]
        wbi_c = c.get("wbi_score")
        wbi_o = o.get("wbi_score")
        nci_c = c.get("nci_score")
        nci_o = o.get("nci_score")
        row_changed = (wbi_c != wbi_o) or (nci_c != nci_o)
        if row_changed:
            changed_rows += 1
            mid = int(c["member_id"])
            changed_member_ids.add(mid)
            if wbi_c != wbi_o:
                changed_wbi_rows += 1
                if wbi_c is not None and wbi_o is not None:
                    member_wbi_deltas.setdefault(mid, []).append(float(wbi_o) - float(wbi_c))
            if nci_c != nci_o:
                changed_nci_rows += 1

    member_delta_summary: List[Tuple[int, float, int]] = []
    for mid, ds in member_wbi_deltas.items():
        if not ds:
            continue
        avg_delta = sum(ds) / len(ds)
        member_delta_summary.append((mid, avg_delta, len(ds)))
    member_delta_summary.sort(key=lambda x: abs(x[1]), reverse=True)

    lines = [
        "# visit_60d_member_detail_with_indices：当前版 vs 优化版",
        "",
        "## 对比概览",
        f"- 当前行数: `{len(current_rows)}`",
        f"- 优化行数: `{len(optimized_rows)}`",
        f"- 共同主键行数(site_id,member_id,visit_time): `{len(common_keys)}`",
        f"- 仅当前有: `{len(cur_keys - opt_keys)}`",
        f"- 仅优化有: `{len(opt_keys - cur_keys)}`",
        f"- 分数发生变化的行: `{changed_rows}`",
        f"- WBI变化行: `{changed_wbi_rows}`",
        f"- NCI变化行: `{changed_nci_rows}`",
        f"- 涉及会员数: `{len(changed_member_ids)}`",
        "",
        "## 经营解读",
        "- 本次优化只改 WBI：把 Overdue 从等权历史替换为时间加权CDF（近期样本权重更高）。",
        "- NCI保持不变，用于避免把两类策略（老客挽回/新客转化）混在一次改动里。",
        "- 若变化主要出现在近期行为变化快的会员，通常更符合一线“近期状态优先”的经营直觉。",
        "",
        "## WBI变化最大会员(按平均分差绝对值)",
        "|member_id|avg_delta(optimized-current)|visit_rows|",
        "|---|---:|---:|",
    ]
    for mid, avg_delta, cnt in member_delta_summary[:20]:
        lines.append(f"|{mid}|{avg_delta:.2f}|{cnt}|")
    if len(member_delta_summary) == 0:
        lines.append("|(none)|0.00|0|")

    out_md.parent.mkdir(parents=True, exist_ok=True)
    out_md.write_text("\n".join(lines), encoding="utf-8-sig")


def main() -> None:
    parser = argparse.ArgumentParser(description="Export 60-day member visit detail with WBI/NCI scores.")
    parser.add_argument("--site-id", type=int, default=None, help="Site id to export")
    parser.add_argument("--days", type=int, default=60, help="Lookback days (default: 60)")
    parser.add_argument(
        "--scheme",
        choices=["current", "optimized", "both"],
        default="current",
        help="Export scheme",
    )
    parser.add_argument(
        "--wbi-interval-halflife-days",
        type=float,
        default=30.0,
        help="Half-life days for weighted CDF in optimized WBI",
    )
    parser.add_argument(
        "--output-csv",
        default=os.path.join(ROOT, "docs", "visit_60d_member_detail_with_indices.csv"),
        help="Output CSV path (used by current/optimized single scheme)",
    )
    parser.add_argument(
        "--output-preview-md",
        default=os.path.join(ROOT, "docs", "visit_60d_member_detail_with_indices_preview.md"),
        help="Output preview markdown path (used by current/optimized single scheme)",
    )
    parser.add_argument(
        "--output-csv-current",
        default=os.path.join(ROOT, "docs", "visit_60d_member_detail_with_indices_current.csv"),
        help="Output CSV path for current scheme when --scheme both",
    )
    parser.add_argument(
        "--output-csv-optimized",
        default=os.path.join(ROOT, "docs", "visit_60d_member_detail_with_indices_optimized.csv"),
        help="Output CSV path for optimized scheme when --scheme both",
    )
    parser.add_argument(
        "--output-compare-md",
        default=os.path.join(ROOT, "docs", "visit_60d_member_detail_with_indices_compare.md"),
        help="Output compare markdown path when --scheme both",
    )
    parser.add_argument("--preview-limit", type=int, default=200, help="Preview markdown row limit")
    args = parser.parse_args()

    config = AppConfig.load()
    db_conn = DatabaseConnection(config.config["db"]["dsn"])
    db = DatabaseOperations(db_conn)
    try:
        site_id = _resolve_site_id(config, db, args.site_id)
        now = datetime.now()
        start_time = now - timedelta(days=max(1, int(args.days)))
        end_time = now

        base_rows = _fetch_visit_rows_base(db, site_id, start_time, end_time)
        wbi_current_map, nci_current_map = _fetch_current_score_maps(db, site_id)

        if args.scheme == "current":
            rows = _attach_scores(base_rows, wbi_current_map, nci_current_map)
            out_csv = Path(args.output_csv)
            out_md = Path(args.output_preview_md)
            _write_csv(rows, out_csv)
            _write_preview_md(rows, out_md, limit=max(1, int(args.preview_limit)))
            print(f"site_id={site_id}")
            print("scheme=current")
            print(f"rows={len(rows)}")
            print(f"csv={out_csv}")
            print(f"preview={out_md}")
            return

        wbi_optimized_map = _build_wbi_optimized_map(
            db=db,
            site_id=site_id,
            base_date=end_time.date(),
            half_life_days=max(1.0, float(args.wbi_interval_halflife_days)),
        )

        if args.scheme == "optimized":
            rows = _attach_scores(base_rows, wbi_optimized_map, nci_current_map)
            out_csv = Path(args.output_csv)
            out_md = Path(args.output_preview_md)
            _write_csv(rows, out_csv)
            _write_preview_md(rows, out_md, limit=max(1, int(args.preview_limit)))
            print(f"site_id={site_id}")
            print("scheme=optimized")
            print(f"rows={len(rows)}")
            print(f"csv={out_csv}")
            print(f"preview={out_md}")
            return

        current_rows = _attach_scores(base_rows, wbi_current_map, nci_current_map)
        optimized_rows = _attach_scores(base_rows, wbi_optimized_map, nci_current_map)

        out_cur = Path(args.output_csv_current)
        out_opt = Path(args.output_csv_optimized)
        out_cmp = Path(args.output_compare_md)
        _write_csv(current_rows, out_cur)
        _write_csv(optimized_rows, out_opt)
        _diff_and_write_report(current_rows, optimized_rows, out_cmp)
        print(f"site_id={site_id}")
        print("scheme=both")
        print(f"rows={len(current_rows)}")
        print(f"csv_current={out_cur}")
        print(f"csv_optimized={out_opt}")
        print(f"compare={out_cmp}")
    finally:
        db_conn.close()


if __name__ == "__main__":
    main()
