#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""PChome / MOMO 競價情報共用資料出口。

短期 canonical source：
- competitor_prices：目前有效配對
- competitor_price_history：價格歷史趨勢
- competitor_match_attempts：未配對與低信心診斷
"""

from __future__ import annotations

import os
import pickle
import time
from datetime import date, datetime, timedelta
from pathlib import Path
from threading import Lock
from typing import Any, Optional, Union

from sqlalchemy import inspect, text


PCHOME_MATCH_SCORE_FLOOR = 0.76
COMPETITOR_INTEL_CACHE_TTL_SECONDS = int(os.getenv("COMPETITOR_INTEL_CACHE_TTL_SECONDS", "1800"))
_BASE_DIR = Path(__file__).resolve().parents[1]
_CACHE_FILE = _BASE_DIR / "data" / "competitor_intel_cache.pkl"
_CACHE_LOCK = Lock()
_MEM_CACHE: dict[str, dict[str, Any]] = {}


def _num(value: Any) -> float:
    try:
        return float(value or 0)
    except (TypeError, ValueError):
        return 0.0


def _date_label(value: Any) -> str:
    if hasattr(value, "strftime"):
        return value.strftime("%Y-%m-%d")
    return str(value or "")


def _month_label(value: Any) -> str:
    if hasattr(value, "strftime"):
        return value.strftime("%Y-%m")
    return str(value or "")[:7]


def clear_competitor_intel_cache() -> None:
    """Clear cached PChome/MOMO intelligence after crawler/import updates."""
    with _CACHE_LOCK:
        _MEM_CACHE.clear()
        try:
            if _CACHE_FILE.exists():
                _CACHE_FILE.unlink()
        except OSError:
            pass


def _load_shared_cache() -> dict[str, dict[str, Any]]:
    if not _CACHE_FILE.exists():
        return {}
    try:
        with _CACHE_FILE.open("rb") as handle:
            payload = pickle.load(handle)
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def _write_shared_cache(payload: dict[str, dict[str, Any]]) -> None:
    try:
        _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        tmp_file = _CACHE_FILE.with_suffix(f".{os.getpid()}.tmp")
        with tmp_file.open("wb") as handle:
            pickle.dump(payload, handle, protocol=pickle.HIGHEST_PROTOCOL)
        os.replace(tmp_file, _CACHE_FILE)
    except Exception:
        try:
            if "tmp_file" in locals() and tmp_file.exists():
                tmp_file.unlink()
        except OSError:
            pass


def _cached_payload(cache_key: str, producer, ttl_seconds: int = COMPETITOR_INTEL_CACHE_TTL_SECONDS):
    if ttl_seconds <= 0:
        return producer()
    now = time.time()
    with _CACHE_LOCK:
        entry = _MEM_CACHE.get(cache_key)
        if entry and now - float(entry.get("time", 0)) < ttl_seconds:
            return entry.get("value")
        shared = _load_shared_cache()
        entry = shared.get(cache_key)
        if entry and now - float(entry.get("time", 0)) < ttl_seconds:
            _MEM_CACHE[cache_key] = entry
            return entry.get("value")

    value = producer()
    entry = {"time": now, "value": value}
    with _CACHE_LOCK:
        _MEM_CACHE[cache_key] = entry
        shared = _load_shared_cache()
        shared[cache_key] = entry
        stale_before = now - max(ttl_seconds * 4, 3600)
        shared = {
            key: item
            for key, item in shared.items()
            if isinstance(item, dict) and float(item.get("time", 0)) >= stale_before
        }
        _write_shared_cache(shared)
    return value


def fetch_competitor_coverage(engine) -> dict:
    return _cached_payload(
        f"coverage:v2:floor={PCHOME_MATCH_SCORE_FLOOR}",
        lambda: _fetch_competitor_coverage_uncached(engine),
    )


def _fetch_competitor_coverage_uncached(engine) -> dict:
    """讀取目前 PChome 比價覆蓋率與待審分類。"""
    if not inspect(engine).has_table("competitor_prices"):
        return {
            "active_with_price": 0,
            "valid_matches": 0,
            "pending": 0,
            "match_rate": 0,
            "attempt_status": {},
        }

    sql = text(f"""
        WITH latest_momo AS (
            SELECT
                p.id AS product_id,
                p.i_code AS sku,
                pr.price AS momo_price,
                ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pr.timestamp DESC, pr.id DESC) AS rn
            FROM products p
            JOIN price_records pr ON pr.product_id = p.id
            WHERE p.status = 'ACTIVE'
        ),
        valid_competitor AS (
            SELECT DISTINCT ON (cp.sku)
                cp.sku
            FROM competitor_prices cp
            WHERE cp.source = 'pchome'
              AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)
              AND cp.price IS NOT NULL
              AND cp.price > 0
              AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
              AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
            ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
        ),
        latest_attempt AS (
            SELECT DISTINCT ON (sku)
                sku,
                attempt_status
            FROM competitor_match_attempts
            WHERE source = 'pchome'
            ORDER BY sku, attempted_at DESC NULLS LAST
        )
        SELECT
            (SELECT COUNT(*) FROM latest_momo WHERE rn = 1) AS active_with_price,
            (SELECT COUNT(*) FROM valid_competitor) AS valid_matches,
            (SELECT COUNT(*)
             FROM latest_momo lm
             LEFT JOIN valid_competitor vc ON vc.sku = lm.sku
             WHERE lm.rn = 1 AND vc.sku IS NULL) AS pending,
            COALESCE(la.attempt_status, 'never_attempted') AS attempt_status,
            COUNT(*) AS status_count
        FROM latest_momo lm
        LEFT JOIN valid_competitor vc ON vc.sku = lm.sku
        LEFT JOIN latest_attempt la ON la.sku = lm.sku
        WHERE lm.rn = 1
          AND vc.sku IS NULL
        GROUP BY COALESCE(la.attempt_status, 'never_attempted')
    """)
    with engine.connect() as conn:
        rows = conn.execute(sql).mappings().all()

    active = int(rows[0].get("active_with_price") or 0) if rows else 0
    valid = int(rows[0].get("valid_matches") or 0) if rows else 0
    pending = int(rows[0].get("pending") or 0) if rows else 0
    statuses = {
        str(row.get("attempt_status")): int(row.get("status_count") or 0)
        for row in rows
    }
    return {
        "active_with_price": active,
        "valid_matches": valid,
        "pending": pending,
        "match_rate": round(valid / max(active, 1) * 100, 1),
        "attempt_status": statuses,
        "match_score_floor": PCHOME_MATCH_SCORE_FLOOR,
    }


def fetch_competitor_gap_trend(engine, days: int = 30) -> dict:
    days = max(7, min(int(days or 30), 120))
    return _cached_payload(
        f"gap_trend:v2:days={days}:floor={PCHOME_MATCH_SCORE_FLOOR}",
        lambda: _fetch_competitor_gap_trend_uncached(engine, days=days),
    )


def _fetch_competitor_gap_trend_uncached(engine, days: int = 30) -> dict:
    """近 N 天 PChome 價差壓力趨勢。"""
    if not inspect(engine).has_table("competitor_price_history"):
        return {"labels": [], "avg_gap_pct": [], "risk_count": [], "momo_advantage_count": [], "match_count": []}

    days = max(7, min(int(days or 30), 120))
    sql = text(f"""
        WITH latest_history AS (
            SELECT
                date_trunc('day', cph.crawled_at)::date AS bucket_date,
                cph.sku,
                cph.momo_price,
                cph.price AS pchome_price,
                ROW_NUMBER() OVER (
                    PARTITION BY date_trunc('day', cph.crawled_at)::date, cph.sku
                    ORDER BY cph.crawled_at DESC
                ) AS rn
            FROM competitor_price_history cph
            WHERE cph.source = 'pchome'
              AND cph.crawled_at >= CURRENT_DATE - (:days * INTERVAL '1 day')
              AND cph.momo_price IS NOT NULL
              AND cph.momo_price > 0
              AND cph.price IS NOT NULL
              AND cph.price > 0
              AND COALESCE(cph.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
              AND COALESCE(cph.tags, '[]'::jsonb) ? 'identity_v2'
        )
        SELECT
            bucket_date,
            COUNT(*) AS match_count,
            ROUND(AVG((momo_price - pchome_price) / pchome_price * 100)::numeric, 2) AS avg_gap_pct,
            SUM(CASE WHEN momo_price > pchome_price * 1.05 THEN 1 ELSE 0 END) AS risk_count,
            SUM(CASE WHEN momo_price < pchome_price * 0.95 THEN 1 ELSE 0 END) AS momo_advantage_count
        FROM latest_history
        WHERE rn = 1
        GROUP BY bucket_date
        ORDER BY bucket_date
    """)
    with engine.connect() as conn:
        rows = conn.execute(sql, {"days": days}).mappings().all()

    return {
        "labels": [_date_label(row.get("bucket_date")) for row in rows],
        "avg_gap_pct": [_num(row.get("avg_gap_pct")) for row in rows],
        "risk_count": [int(row.get("risk_count") or 0) for row in rows],
        "momo_advantage_count": [int(row.get("momo_advantage_count") or 0) for row in rows],
        "match_count": [int(row.get("match_count") or 0) for row in rows],
    }


def fetch_competitor_monthly_pressure(engine, months: int = 12) -> dict:
    months = max(3, min(int(months or 12), 36))
    return _cached_payload(
        f"monthly_pressure:v2:months={months}:floor={PCHOME_MATCH_SCORE_FLOOR}",
        lambda: _fetch_competitor_monthly_pressure_uncached(engine, months=months),
    )


def _fetch_competitor_monthly_pressure_uncached(engine, months: int = 12) -> dict:
    """月度競品價格壓力，用於 growth analysis。"""
    if not inspect(engine).has_table("competitor_price_history"):
        return {"labels": [], "avg_gap_pct": [], "risk_count": [], "match_count": []}

    months = max(3, min(int(months or 12), 36))
    sql = text(f"""
        WITH latest_history AS (
            SELECT
                date_trunc('month', cph.crawled_at)::date AS bucket_month,
                cph.sku,
                cph.momo_price,
                cph.price AS pchome_price,
                ROW_NUMBER() OVER (
                    PARTITION BY date_trunc('month', cph.crawled_at)::date, cph.sku
                    ORDER BY cph.crawled_at DESC
                ) AS rn
            FROM competitor_price_history cph
            WHERE cph.source = 'pchome'
              AND cph.crawled_at >= date_trunc('month', CURRENT_DATE) - (:months * INTERVAL '1 month')
              AND cph.momo_price IS NOT NULL
              AND cph.momo_price > 0
              AND cph.price IS NOT NULL
              AND cph.price > 0
              AND COALESCE(cph.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
              AND COALESCE(cph.tags, '[]'::jsonb) ? 'identity_v2'
        )
        SELECT
            bucket_month,
            COUNT(*) AS match_count,
            ROUND(AVG((momo_price - pchome_price) / pchome_price * 100)::numeric, 2) AS avg_gap_pct,
            SUM(CASE WHEN momo_price > pchome_price * 1.05 THEN 1 ELSE 0 END) AS risk_count
        FROM latest_history
        WHERE rn = 1
        GROUP BY bucket_month
        ORDER BY bucket_month
    """)
    with engine.connect() as conn:
        rows = conn.execute(sql, {"months": months}).mappings().all()

    return {
        "labels": [_month_label(row.get("bucket_month")) for row in rows],
        "avg_gap_pct": [_num(row.get("avg_gap_pct")) for row in rows],
        "risk_count": [int(row.get("risk_count") or 0) for row in rows],
        "match_count": [int(row.get("match_count") or 0) for row in rows],
    }


def fetch_top_competitor_risks(engine, limit: int = 10) -> list[dict]:
    limit = max(1, min(int(limit or 10), 50))
    return _cached_payload(
        f"top_risks:v2:limit={limit}:floor={PCHOME_MATCH_SCORE_FLOOR}",
        lambda: _fetch_top_competitor_risks_uncached(engine, limit=limit),
    )


def _fetch_top_competitor_risks_uncached(engine, limit: int = 10) -> list[dict]:
    """目前 MOMO 比 PChome 貴的高風險商品。"""
    if not inspect(engine).has_table("competitor_prices"):
        return []

    limit = max(1, min(int(limit or 10), 50))
    sql = text(f"""
        WITH latest_momo AS (
            SELECT
                p.id AS product_id,
                p.i_code AS sku,
                p.name,
                p.category,
                pr.price AS momo_price,
                ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pr.timestamp DESC, pr.id DESC) AS rn
            FROM products p
            JOIN price_records pr ON pr.product_id = p.id
            WHERE p.status = 'ACTIVE'
        ),
        valid_competitor AS (
            SELECT DISTINCT ON (cp.sku)
                cp.sku,
                cp.price AS pchome_price,
                cp.competitor_product_id,
                cp.competitor_product_name,
                cp.match_score,
                cp.crawled_at
            FROM competitor_prices cp
            WHERE cp.source = 'pchome'
              AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)
              AND cp.price IS NOT NULL
              AND cp.price > 0
              AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
              AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
            ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
        )
        SELECT
            lm.sku,
            lm.name,
            lm.category,
            lm.momo_price,
            vc.pchome_price,
            vc.competitor_product_id,
            vc.competitor_product_name,
            vc.match_score,
            vc.crawled_at,
            (lm.momo_price - vc.pchome_price) AS gap_amount,
            ((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100) AS gap_pct
        FROM latest_momo lm
        JOIN valid_competitor vc ON vc.sku = lm.sku
        WHERE lm.rn = 1
          AND lm.momo_price > vc.pchome_price * 1.05
        ORDER BY gap_pct DESC NULLS LAST, gap_amount DESC NULLS LAST
        LIMIT :limit
    """)
    with engine.connect() as conn:
        rows = conn.execute(sql, {"limit": limit}).mappings().all()

    result = []
    for row in rows:
        result.append({
            "sku": str(row.get("sku") or ""),
            "name": row.get("name") or "",
            "category": row.get("category") or "",
            "momo_price": _num(row.get("momo_price")),
            "pchome_price": _num(row.get("pchome_price")),
            "gap_amount": _num(row.get("gap_amount")),
            "gap_pct": _num(row.get("gap_pct")),
            "match_score": _num(row.get("match_score")),
            "pchome_id": row.get("competitor_product_id"),
            "pchome_name": row.get("competitor_product_name") or "",
            "crawled_at": _date_label(row.get("crawled_at")),
        })
    return result


def fetch_competitor_comparison_results(
    engine,
    start_date: Optional[Union[date, datetime, str]] = None,
    end_date: Optional[Union[date, datetime, str]] = None,
    limit: int = 30,
) -> list[dict]:
    """輸出與 legacy competitor PPT 相容的比價結果，不再 live crawl。"""
    limit = max(1, min(int(limit or 30), 100))
    has_daily_sales = inspect(engine).has_table("daily_sales")
    sales_cte = ""
    sales_join = ""
    sales_select = "0 AS momo_revenue,"
    order_expr = "ABS((lm.momo_price - vc.pchome_price) / vc.pchome_price * 100) DESC NULLS LAST"
    params: dict[str, Any] = {"limit": limit}

    if has_daily_sales:
        where = []
        if start_date:
            where.append("DATE(s.date) >= DATE(:start_date)")
            params["start_date"] = str(start_date).replace("/", "-")[:10]
        if end_date:
            where.append("DATE(s.date) <= DATE(:end_date)")
            params["end_date"] = str(end_date).replace("/", "-")[:10]
        sales_where = "WHERE " + " AND ".join(where) if where else ""
        sales_cte = f""",
        sales_rank AS (
            SELECT
                s.product_id,
                SUM(COALESCE(s.revenue, 0)) AS momo_revenue
            FROM daily_sales s
            {sales_where}
            GROUP BY s.product_id
        )
        """
        sales_join = "LEFT JOIN sales_rank sr ON sr.product_id = lm.product_id"
        sales_select = "COALESCE(sr.momo_revenue, 0) AS momo_revenue,"
        order_expr = "COALESCE(sr.momo_revenue, 0) DESC, " + order_expr

    sql = text(f"""
        WITH latest_momo AS (
            SELECT
                p.id AS product_id,
                p.i_code AS sku,
                p.name,
                pr.price AS momo_price,
                ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pr.timestamp DESC, pr.id DESC) AS rn
            FROM products p
            JOIN price_records pr ON pr.product_id = p.id
            WHERE p.status = 'ACTIVE'
        ),
        valid_competitor AS (
            SELECT DISTINCT ON (cp.sku)
                cp.sku,
                cp.price AS pchome_price,
                cp.competitor_product_id,
                cp.competitor_product_name,
                cp.match_score
            FROM competitor_prices cp
            WHERE cp.source = 'pchome'
              AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)
              AND cp.price IS NOT NULL
              AND cp.price > 0
              AND COALESCE(cp.match_score, 0) >= {PCHOME_MATCH_SCORE_FLOOR}
              AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
            ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
        )
        {sales_cte}
        SELECT
            lm.sku,
            lm.name,
            lm.momo_price,
            vc.pchome_price,
            vc.competitor_product_id,
            vc.competitor_product_name,
            vc.match_score,
            {sales_select}
            (vc.pchome_price - lm.momo_price) AS price_diff,
            ((vc.pchome_price - lm.momo_price) / lm.momo_price * 100) AS price_diff_pct
        FROM latest_momo lm
        JOIN valid_competitor vc ON vc.sku = lm.sku
        {sales_join}
        WHERE lm.rn = 1
          AND lm.momo_price > 0
        ORDER BY {order_expr}
        LIMIT :limit
    """)
    with engine.connect() as conn:
        rows = conn.execute(sql, params).mappings().all()

    results = []
    for row in rows:
        pchome_id = row.get("competitor_product_id")
        results.append({
            "found": True,
            "momo_icode": str(row.get("sku") or ""),
            "momo_name": row.get("name") or "",
            "momo_price": _num(row.get("momo_price")),
            "pc_name": row.get("competitor_product_name") or "",
            "pc_price": _num(row.get("pchome_price")),
            "pc_url": f"https://24h.pchome.com.tw/prod/{pchome_id}" if pchome_id else "",
            "price_diff": _num(row.get("price_diff")),
            "price_diff_pct": _num(row.get("price_diff_pct")),
            "match_score": _num(row.get("match_score")),
            "momo_revenue": _num(row.get("momo_revenue")),
        })
    return results


def build_competitor_intel_payload(engine, days: int = 30) -> dict:
    """頁面、AI、PPT 可共用的摘要 payload。"""
    return {
        "coverage": fetch_competitor_coverage(engine),
        "trend": fetch_competitor_gap_trend(engine, days=days),
        "top_risks": fetch_top_competitor_risks(engine, limit=10),
        "match_score_floor": PCHOME_MATCH_SCORE_FLOOR,
    }
