#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
商品看板路由模組
包含：首頁儀表板、商品列表、統計數據
"""

import os
import json
import math
import time
import hashlib
import pickle
from datetime import datetime, timezone, timedelta
from flask import Blueprint, request, render_template
from sqlalchemy import func, and_, text, bindparam
from sqlalchemy.orm import joinedload

from auth import login_required
from config import BASE_DIR, SYSTEM_VERSION, public_url
from database.manager import DatabaseManager
from database.models import Product, PriceRecord
from services.logger_manager import SystemLogger
from services.cache_manager import (
    _DASHBOARD_DATA_CACHE,
    _DASHBOARD_CACHE_TTL,
    _DASHBOARD_SHARED_CACHE_FILE,
    _DASHBOARD_STALE_CACHE_FILE,
)
from utils.momo_url_utils import build_momo_product_url, normalize_momo_product_url

# 時區設定
TAIPEI_TZ = timezone(timedelta(hours=8))

# Logger
sys_log = SystemLogger("DashboardRoutes").get_logger()

# Blueprint 定義
dashboard_bp = Blueprint('dashboard', __name__)

PRODUCT_PICK_LIST_LIMIT = 50


def _build_pchome_product_url(product_id):
    if not product_id:
        return None
    return f"https://24h.pchome.com.tw/prod/{str(product_id).strip()}"


def _build_momo_product_url(i_code):
    return build_momo_product_url(i_code)


def _to_float(value):
    if value is None:
        return None
    try:
        return float(value)
    except (TypeError, ValueError):
        return None


def _build_competitor_decision(momo_price, pchome_price):
    if not pchome_price:
        return {
            'label': '待比對',
            'tone': 'neutral',
            'gap_amount': None,
            'gap_pct': None,
            'summary': '尚無 PChome 對應商品或價格快取'
        }

    momo_price = float(momo_price or 0)
    pchome_price = float(pchome_price)
    gap_amount = momo_price - pchome_price
    gap_pct = (gap_amount / pchome_price * 100) if pchome_price else 0

    if gap_pct >= 5:
        return {
            'label': 'PChome 優勢',
            'tone': 'win',
            'gap_amount': gap_amount,
            'gap_pct': gap_pct,
            'summary': 'PChome 較便宜，可加強曝光與轉換'
        }
    if gap_pct <= -5:
        return {
            'label': 'MOMO 威脅',
            'tone': 'risk',
            'gap_amount': gap_amount,
            'gap_pct': gap_pct,
            'summary': 'MOMO 較便宜，需評估價格或促銷因應'
        }
    return {
        'label': '價格接近',
        'tone': 'watch',
        'gap_amount': gap_amount,
        'gap_pct': gap_pct,
        'summary': '價差有限，建議主打服務、到貨或回饋'
    }


def _load_pchome_competitor_map(session, skus):
    sku_list = [str(sku) for sku in skus if sku]
    if not sku_list:
        return {}

    try:
        stmt = text("""
            SELECT
                sku,
                price,
                original_price,
                discount_pct,
                competitor_product_id,
                competitor_product_name,
                match_score,
                tags,
                crawled_at,
                expires_at
            FROM competitor_prices
            WHERE source = 'pchome'
              AND sku IN :skus
              AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
        """).bindparams(bindparam("skus", expanding=True))
        rows = session.execute(stmt, {"skus": sku_list}).mappings().all()
    except Exception as exc:
        sys_log.warning(f"[Dashboard] PChome 競品價格資料讀取略過: {exc}")
        return {}

    result = {}
    for row in rows:
        competitor_product_id = row.get('competitor_product_id')
        result[str(row.get('sku'))] = {
            'source': 'pchome',
            'price': _to_float(row.get('price')),
            'original_price': _to_float(row.get('original_price')),
            'discount_pct': row.get('discount_pct'),
            'product_id': competitor_product_id,
            'product_name': row.get('competitor_product_name'),
            'product_url': _build_pchome_product_url(competitor_product_id),
            'match_score': _to_float(row.get('match_score')),
            'tags': row.get('tags'),
            'crawled_at': row.get('crawled_at'),
            'expires_at': row.get('expires_at'),
        }
    return result


def _format_dashboard_dt(value):
    if not value:
        return None
    if hasattr(value, "strftime"):
        return value.strftime("%Y-%m-%d %H:%M")
    return str(value)


def _parse_agent_footprint(value):
    if not value:
        return {}
    if isinstance(value, str):
        try:
            value = json.loads(value)
        except Exception:
            return {}
    if not isinstance(value, dict):
        return {}
    agent = value.get('agent')
    return agent if isinstance(agent, dict) else {}


def _ai_pick_evidence_fields(model_footprint):
    agent = _parse_agent_footprint(model_footprint)
    missing_evidence = agent.get('missing_evidence') or []
    if isinstance(missing_evidence, str):
        missing_evidence = [missing_evidence]
    missing_evidence = [str(item) for item in missing_evidence if item]
    return {
        'opportunity_score': _to_float(agent.get('opportunity_score')) or 0,
        'evidence_quality': _to_float(agent.get('evidence_quality')) or 0,
        'confidence_band': agent.get('confidence_band') or 'needs_evidence',
        'missing_evidence': missing_evidence,
        'missing_evidence_text': '、'.join(missing_evidence),
        'margin_rate': _to_float(agent.get('margin_rate')),
    }


def _dashboard_decision_row(row, tone):
    sku = str(row.get('sku') or '')
    pchome_id = row.get('competitor_product_id')
    momo_url = normalize_momo_product_url(row.get('momo_url'), sku) or _build_momo_product_url(sku)
    return {
        'sku': sku,
        'name': row.get('name') or '',
        'category': row.get('category') or '',
        'momo_price': _to_float(row.get('momo_price')) or 0,
        'pchome_price': _to_float(row.get('pchome_price')) or 0,
        'gap_pct': _to_float(row.get('gap_pct')) or 0,
        'gap_amount': _to_float(row.get('gap_amount')) or 0,
        'confidence': _to_float(row.get('confidence')),
        'reason': row.get('reason') or '',
        'tone': tone,
        'momo_url': momo_url,
        'pchome_id': pchome_id,
        'pchome_name': row.get('competitor_product_name') or '',
        'pchome_url': _build_pchome_product_url(pchome_id),
        'crawled_at': _format_dashboard_dt(row.get('crawled_at') or row.get('created_at')),
        **_ai_pick_evidence_fields(row.get('model_footprint')),
    }


def _load_competitor_decision_overview(session, latest_items=None):
    """讀取商品看板第一屏使用的 PChome 比價決策摘要。全部來自正式 DB。"""
    cache_key = 'competitor_decision_overview'
    cache_ts_key = 'competitor_decision_overview_timestamp'
    cached = _DASHBOARD_DATA_CACHE.get(cache_key)
    cached_ts = _DASHBOARD_DATA_CACHE.get(cache_ts_key)
    if cached and cached_ts:
        age = time.time() - cached_ts
        if age < min(_DASHBOARD_CACHE_TTL, 300):
            return cached

    default = {
        'total_active': 0,
        'matched_count': 0,
        'match_rate': 0,
        'pchome_advantage_count': 0,
        'momo_threat_count': 0,
        'near_count': 0,
        'pending_match_count': 0,
        'ai_pick_count': 0,
        'avg_advantage_gap': 0,
        'last_pchome_crawled': None,
        'top_picks': [],
        'top_pchome_advantages': [],
        'top_momo_threats': [],
        'pending_priority': [],
    }

    if latest_items:
        try:
            item_map = {}
            for item in latest_items:
                record = item.get('record')
                product = getattr(record, 'product', None)
                sku = str(getattr(product, 'i_code', '') or '')
                if not sku:
                    continue
                safe_product_url = normalize_momo_product_url(getattr(product, 'url', None), sku)
                item_map[sku] = {
                    'sku': sku,
                    'name': getattr(product, 'name', '') or '',
                    'category': getattr(product, 'category', '') or '',
                    'momo_url': safe_product_url or _build_momo_product_url(sku),
                    'momo_price': _to_float(getattr(record, 'price', None)) or 0,
                }

            competitor_map = _load_pchome_competitor_map(session, item_map.keys())
            compared = []
            for sku, item in item_map.items():
                competitor = competitor_map.get(sku)
                pchome_price = _to_float(competitor.get('price')) if competitor else None
                if not pchome_price:
                    continue
                gap_amount = item['momo_price'] - pchome_price
                gap_pct = gap_amount / pchome_price * 100 if pchome_price else 0
                compared.append({
                    **item,
                    'pchome_price': pchome_price,
                    'competitor_product_id': competitor.get('product_id'),
                    'competitor_product_name': competitor.get('product_name'),
                    'match_score': competitor.get('match_score'),
                    'crawled_at': competitor.get('crawled_at'),
                    'gap_amount': gap_amount,
                    'gap_pct': gap_pct,
                })

            picks_rows = session.execute(text("""
                SELECT
                    sku,
                    name,
                    momo_price,
                    pchome_price,
                    gap_pct,
                    confidence,
                    reason,
                    model_footprint,
                    created_at
                FROM ai_price_recommendations
                WHERE strategy = 'product_pick'
                  AND status = 'pending'
                ORDER BY confidence DESC NULLS LAST, gap_pct DESC NULLS LAST, created_at DESC
                LIMIT 3
            """)).mappings().all()
            ai_pick_count = session.execute(text("""
                SELECT COUNT(*)
                FROM ai_price_recommendations
                WHERE strategy = 'product_pick'
                  AND status = 'pending'
            """)).scalar() or 0

            total_active = len(item_map)
            matched_count = len(compared)
            pchome_advantages = [row for row in compared if row['gap_pct'] >= 5]
            momo_threats = [row for row in compared if row['gap_pct'] <= -5]
            near_items = [row for row in compared if -5 < row['gap_pct'] < 5]
            pending_items = [
                row for sku, row in item_map.items()
                if sku not in competitor_map
            ]
            last_crawled = max(
                (row.get('crawled_at') for row in compared if row.get('crawled_at')),
                default=None,
            )

            overview = dict(default)
            overview.update({
                'total_active': total_active,
                'matched_count': matched_count,
                'match_rate': round(matched_count / max(total_active, 1) * 100, 1),
                'pchome_advantage_count': len(pchome_advantages),
                'momo_threat_count': len(momo_threats),
                'near_count': len(near_items),
                'pending_match_count': max(total_active - matched_count, 0),
                'ai_pick_count': int(ai_pick_count),
                'avg_advantage_gap': round(
                    sum(row['gap_pct'] for row in pchome_advantages) / len(pchome_advantages),
                    1,
                ) if pchome_advantages else 0,
                'last_pchome_crawled': _format_dashboard_dt(last_crawled),
            })
            overview['top_pchome_advantages'] = [
                _dashboard_decision_row(row, 'win')
                for row in sorted(pchome_advantages, key=lambda row: row['gap_pct'], reverse=True)[:3]
            ]
            overview['top_momo_threats'] = [
                _dashboard_decision_row(row, 'risk')
                for row in sorted(momo_threats, key=lambda row: row['gap_pct'])[:3]
            ]
            overview['top_picks'] = []
            for row in picks_rows:
                pick = dict(row)
                competitor = competitor_map.get(str(pick.get('sku') or '')) or {}
                pick['competitor_product_id'] = competitor.get('product_id')
                pick['competitor_product_name'] = competitor.get('product_name')
                pick['crawled_at'] = competitor.get('crawled_at')
                overview['top_picks'].append(_dashboard_decision_row(pick, 'pick'))
            overview['pending_priority'] = [
                {
                    'sku': row['sku'],
                    'name': row['name'],
                    'category': row['category'],
                    'momo_price': row['momo_price'],
                    'momo_url': normalize_momo_product_url(row.get('momo_url'), row.get('sku')) or _build_momo_product_url(row.get('sku')),
                }
                for row in sorted(pending_items, key=lambda row: row['momo_price'], reverse=True)[:3]
            ]
            _DASHBOARD_DATA_CACHE[cache_key] = overview
            _DASHBOARD_DATA_CACHE[cache_ts_key] = time.time()
            return overview
        except Exception as exc:
            sys_log.warning(f"[Dashboard] PChome 比價快取摘要讀取略過，改用 SQL 後備: {exc}")
            try:
                session.rollback()
            except Exception:
                pass

    latest_compared_cte = """
        WITH latest_momo AS (
            SELECT
                p.id AS product_id,
                p.i_code AS sku,
                p.name,
                p.url AS momo_url,
                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'
        ),
        latest_products AS (
            SELECT * FROM latest_momo WHERE rn = 1
        ),
        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) >= 0.42
            ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
        ),
        compared AS (
            SELECT
                lp.*,
                vc.pchome_price,
                vc.competitor_product_id,
                vc.competitor_product_name,
                vc.match_score,
                vc.crawled_at,
                (lp.momo_price - vc.pchome_price) AS gap_amount,
                ((lp.momo_price - vc.pchome_price) / vc.pchome_price * 100) AS gap_pct
            FROM latest_products lp
            JOIN valid_competitor vc ON vc.sku = lp.sku
        )
    """

    stats_sql = text(latest_compared_cte + """
        SELECT
            (SELECT COUNT(*) FROM products WHERE status = 'ACTIVE') AS total_active,
            (SELECT COUNT(*) FROM compared) AS matched_count,
            (SELECT COUNT(*) FROM compared WHERE gap_pct >= 5) AS pchome_advantage_count,
            (SELECT COUNT(*) FROM compared WHERE gap_pct <= -5) AS momo_threat_count,
            (SELECT COUNT(*) FROM compared WHERE gap_pct > -5 AND gap_pct < 5) AS near_count,
            (SELECT COALESCE(ROUND(AVG(gap_pct)::numeric, 1), 0) FROM compared WHERE gap_pct >= 5) AS avg_advantage_gap,
            (SELECT COUNT(*) FROM ai_price_recommendations WHERE strategy = 'product_pick' AND status = 'pending') AS ai_pick_count,
            (SELECT MAX(crawled_at) FROM competitor_prices WHERE source = 'pchome') AS last_pchome_crawled
    """)

    advantage_sql = text(latest_compared_cte + """
        SELECT *
        FROM compared
        WHERE gap_pct >= 5
        ORDER BY gap_pct DESC NULLS LAST, crawled_at DESC NULLS LAST
        LIMIT 3
    """)

    threat_sql = text(latest_compared_cte + """
        SELECT *
        FROM compared
        WHERE gap_pct <= -5
        ORDER BY gap_pct ASC NULLS LAST, crawled_at DESC NULLS LAST
        LIMIT 3
    """)

    pending_sql = text("""
        WITH latest_momo AS (
            SELECT
                p.i_code AS sku,
                p.name,
                p.url AS momo_url,
                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'
        )
        SELECT lm.*
        FROM latest_momo lm
        LEFT JOIN competitor_prices cp
          ON cp.sku = lm.sku
         AND 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) >= 0.42
        WHERE lm.rn = 1
          AND cp.sku IS NULL
        ORDER BY lm.momo_price DESC NULLS LAST
        LIMIT 3
    """)

    picks_sql = text("""
        WITH valid_competitor AS (
            SELECT DISTINCT ON (cp.sku)
                cp.sku,
                cp.competitor_product_id,
                cp.competitor_product_name,
                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) >= 0.42
            ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
        )
        SELECT
            ar.sku,
            ar.name,
            ar.momo_price,
            ar.pchome_price,
            ar.gap_pct,
            ar.confidence,
            ar.reason,
            ar.model_footprint,
            ar.created_at,
            vc.competitor_product_id,
            vc.competitor_product_name,
            vc.crawled_at
        FROM ai_price_recommendations ar
        LEFT JOIN valid_competitor vc ON vc.sku = ar.sku
        WHERE ar.strategy = 'product_pick'
          AND ar.status = 'pending'
        ORDER BY ar.confidence DESC NULLS LAST, ar.gap_pct DESC NULLS LAST, ar.created_at DESC
        LIMIT 3
    """)

    try:
        stats = session.execute(stats_sql).mappings().first()
        overview = dict(default)
        if stats:
            total_active = int(stats.get('total_active') or 0)
            matched_count = int(stats.get('matched_count') or 0)
            overview.update({
                'total_active': total_active,
                'matched_count': matched_count,
                'match_rate': round(matched_count / max(total_active, 1) * 100, 1),
                'pchome_advantage_count': int(stats.get('pchome_advantage_count') or 0),
                'momo_threat_count': int(stats.get('momo_threat_count') or 0),
                'near_count': int(stats.get('near_count') or 0),
                'pending_match_count': max(total_active - matched_count, 0),
                'ai_pick_count': int(stats.get('ai_pick_count') or 0),
                'avg_advantage_gap': _to_float(stats.get('avg_advantage_gap')) or 0,
                'last_pchome_crawled': _format_dashboard_dt(stats.get('last_pchome_crawled')),
            })

        overview['top_pchome_advantages'] = [
            _dashboard_decision_row(row, 'win')
            for row in session.execute(advantage_sql).mappings().all()
        ]
        overview['top_momo_threats'] = [
            _dashboard_decision_row(row, 'risk')
            for row in session.execute(threat_sql).mappings().all()
        ]
        overview['top_picks'] = [
            _dashboard_decision_row(row, 'pick')
            for row in session.execute(picks_sql).mappings().all()
        ]
        overview['pending_priority'] = [
            {
                'sku': str(row.get('sku') or ''),
                'name': row.get('name') or '',
                'category': row.get('category') or '',
                'momo_price': _to_float(row.get('momo_price')) or 0,
                'momo_url': normalize_momo_product_url(row.get('momo_url'), row.get('sku')) or _build_momo_product_url(row.get('sku')),
            }
            for row in session.execute(pending_sql).mappings().all()
        ]
        _DASHBOARD_DATA_CACHE[cache_key] = overview
        _DASHBOARD_DATA_CACHE[cache_ts_key] = time.time()
        return overview
    except Exception as exc:
        sys_log.warning(f"[Dashboard] PChome 比價決策摘要讀取略過: {exc}")
        try:
            session.rollback()
        except Exception:
            pass
        _DASHBOARD_DATA_CACHE[cache_key] = default
        _DASHBOARD_DATA_CACHE[cache_ts_key] = time.time()
        return default


def _load_ai_pick_selection(session, limit=PRODUCT_PICK_LIST_LIMIT):
    """讀取商品看板 AI 挑品清單排序，供列表篩選使用。"""
    sql = text("""
        SELECT
            sku,
            confidence,
            reason,
            momo_price,
            pchome_price,
            gap_pct,
            model_footprint,
            created_at
        FROM ai_price_recommendations
        WHERE strategy = 'product_pick'
          AND status = 'pending'
        ORDER BY confidence DESC NULLS LAST, gap_pct DESC NULLS LAST, created_at DESC
        LIMIT :limit
    """)

    try:
        rows = session.execute(sql, {"limit": limit}).mappings().all()
    except Exception as exc:
        sys_log.warning(f"[Dashboard] AI 挑品清單讀取略過: {exc}")
        try:
            session.rollback()
        except Exception:
            pass
        return [], {}

    skus = []
    pick_map = {}
    for idx, row in enumerate(rows, start=1):
        sku = str(row.get('sku') or '')
        if not sku or sku in pick_map:
            continue
        skus.append(sku)
        pick_map[sku] = {
            'rank': idx,
            'confidence': _to_float(row.get('confidence')) or 0,
            'reason': row.get('reason') or '',
            'momo_price': _to_float(row.get('momo_price')) or 0,
            'pchome_price': _to_float(row.get('pchome_price')) or 0,
            'gap_pct': _to_float(row.get('gap_pct')) or 0,
            'created_at': _format_dashboard_dt(row.get('created_at')),
            **_ai_pick_evidence_fields(row.get('model_footprint')),
        }
    return skus, pick_map


def _summarize_ai_pick_selection(ai_pick_map):
    """彙整目前 AI 挑品清單的可操作摘要，全部來自 ai_price_recommendations。"""
    picks = list(ai_pick_map.values())
    if not picks:
        return {
            'count': 0,
            'avg_confidence': 0,
            'avg_evidence_quality': 0,
            'avg_opportunity_score': 0,
            'avg_gap_pct': 0,
            'max_gap_pct': 0,
            'total_gap_amount': 0,
            'high_confidence_count': 0,
            'needs_evidence_count': 0,
            'top_missing_evidence': [],
            'generated_at': None,
        }

    confidence_values = [pick.get('confidence', 0) for pick in picks]
    evidence_values = [pick.get('evidence_quality', 0) for pick in picks]
    opportunity_values = [pick.get('opportunity_score', 0) for pick in picks]
    gap_values = [pick.get('gap_pct', 0) for pick in picks]
    missing_counts = {}
    for pick in picks:
        for label in pick.get('missing_evidence', []):
            missing_counts[label] = missing_counts.get(label, 0) + 1
    total_gap_amount = sum(
        max((pick.get('momo_price') or 0) - (pick.get('pchome_price') or 0), 0)
        for pick in picks
    )

    return {
        'count': len(picks),
        'avg_confidence': round(sum(confidence_values) / len(confidence_values), 3),
        'avg_evidence_quality': round(sum(evidence_values) / len(evidence_values), 1),
        'avg_opportunity_score': round(sum(opportunity_values) / len(opportunity_values), 1),
        'avg_gap_pct': round(sum(gap_values) / len(gap_values), 1),
        'max_gap_pct': round(max(gap_values), 1),
        'total_gap_amount': round(total_gap_amount),
        'high_confidence_count': sum(1 for value in confidence_values if value >= 0.65),
        'needs_evidence_count': sum(1 for pick in picks if pick.get('confidence_band') == 'needs_evidence'),
        'top_missing_evidence': [
            {'label': label, 'count': count}
            for label, count in sorted(missing_counts.items(), key=lambda item: item[1], reverse=True)[:3]
        ],
        'generated_at': max(
            (pick.get('created_at') for pick in picks if pick.get('created_at')),
            default=None,
        ),
    }


# ==========================================
# 快取與監控變數
# ==========================================
import fcntl

_DASHBOARD_LOCK_FILE = os.path.join(BASE_DIR, 'data', '.dashboard_cache.lock')  # V-Opt: 檔案鎖（跨進程）


class FileLock:
    """簡單的檔案鎖，用於 gunicorn 多進程環境"""
    def __init__(self, lock_file):
        self.lock_file = lock_file
        self.fd = None

    def acquire(self, blocking=True):
        """取得鎖"""
        try:
            self.fd = open(self.lock_file, 'w')
            if blocking:
                fcntl.flock(self.fd, fcntl.LOCK_EX)
            else:
                fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            return True
        except (IOError, OSError):
            if self.fd:
                self.fd.close()
                self.fd = None
            return False

    def release(self):
        """釋放鎖"""
        if self.fd:
            fcntl.flock(self.fd, fcntl.LOCK_UN)
            self.fd.close()
            self.fd = None


_DASHBOARD_STALE_CACHE_MAX_AGE = 86400


def _new_dashboard_file_lock():
    return FileLock(_DASHBOARD_LOCK_FILE)


def _load_dashboard_cache_file(now, cache_path, *, allow_stale=False, label='共享'):
    """讀取跨 worker 商品看板深度快取；必要時可讀舊快取救援首屏。"""
    cache_file = str(cache_path)
    if not os.path.exists(cache_file):
        return None

    try:
        with open(cache_file, 'rb') as f:
            payload = pickle.load(f)

        full_timestamp = payload.get('full_timestamp')
        full_data = payload.get('full_data')
        if not full_timestamp or not full_data:
            return None

        age = now.timestamp() - full_timestamp
        max_age = _DASHBOARD_STALE_CACHE_MAX_AGE if allow_stale else _DASHBOARD_CACHE_TTL
        if age >= max_age:
            return None

        _DASHBOARD_DATA_CACHE['full_data'] = full_data
        _DASHBOARD_DATA_CACHE['full_timestamp'] = full_timestamp
        _DASHBOARD_DATA_CACHE['consolidated_data'] = payload.get('consolidated_data')
        _DASHBOARD_DATA_CACHE['consolidated_timestamp'] = payload.get('consolidated_timestamp')
        _DASHBOARD_DATA_CACHE['today_start'] = payload.get('today_start')
        level = sys_log.info if allow_stale and age >= _DASHBOARD_CACHE_TTL else sys_log.debug
        level(f"[Dashboard] [Cache] ✅ 使用{label}完整看板快取 | 快取年齡: {age:.0f}秒")
        return full_data
    except Exception as exc:
        sys_log.warning(f"[Dashboard] [Cache] {label}快取讀取失敗，改走資料庫重建: {exc}")
        return None


def _load_shared_full_dashboard_cache(now):
    return _load_dashboard_cache_file(now, _DASHBOARD_SHARED_CACHE_FILE)


def _load_stale_full_dashboard_cache(now):
    stale_data = _load_dashboard_cache_file(
        now,
        _DASHBOARD_STALE_CACHE_FILE,
        allow_stale=True,
        label='舊版救援',
    )
    if stale_data:
        return stale_data
    return _load_dashboard_cache_file(
        now,
        _DASHBOARD_SHARED_CACHE_FILE,
        allow_stale=True,
        label='過期救援',
    )


def _write_shared_full_dashboard_cache(full_data):
    """原子寫入跨 worker 共享的商品看板深度快取。"""
    cache_file = str(_DASHBOARD_SHARED_CACHE_FILE)
    tmp_file = f"{cache_file}.{os.getpid()}.tmp"
    payload = {
        'full_data': full_data,
        'full_timestamp': _DASHBOARD_DATA_CACHE.get('full_timestamp'),
        'consolidated_data': _DASHBOARD_DATA_CACHE.get('consolidated_data'),
        'consolidated_timestamp': _DASHBOARD_DATA_CACHE.get('consolidated_timestamp'),
        'today_start': _DASHBOARD_DATA_CACHE.get('today_start'),
    }
    try:
        os.makedirs(os.path.dirname(cache_file), exist_ok=True)
        with open(tmp_file, 'wb') as f:
            pickle.dump(payload, f, protocol=pickle.HIGHEST_PROTOCOL)
        os.replace(tmp_file, cache_file)
    except Exception as exc:
        sys_log.warning(f"[Dashboard] [Cache] 共享快取寫入失敗，仍保留記憶體快取: {exc}")
        try:
            if os.path.exists(tmp_file):
                os.remove(tmp_file)
        except OSError:
            pass


def warm_full_dashboard_cache(reason='manual', force_rebuild=False):
    """供 API、排程與 Gunicorn worker 啟動時預熱商品看板完整快取。"""
    started = time.time()
    data = get_full_dashboard_data(force_rebuild=force_rebuild)
    duration_ms = (time.time() - started) * 1000
    sys_log.info(
        f"[Dashboard] [Cache] ✅ 預熱完成 | reason={reason} | "
        f"items={len(data.get('unique_items', [])) if data else 0} | 耗時={duration_ms:.0f}ms"
    )
    return data

# 慢查詢監控
_SLOW_QUERY_STATS = {
    'total_queries': 0,
    'slow_queries': 0,
    'very_slow_queries': 0,
    'total_query_time_ms': 0,
    'last_slow_query': None,
    'last_slow_query_time': None,
}
_SLOW_QUERY_THRESHOLD_MS = 1000
_VERY_SLOW_QUERY_THRESHOLD_MS = 5000


def track_query_time(query_name, duration_ms):
    """追蹤查詢時間，更新慢查詢統計"""
    global _SLOW_QUERY_STATS
    _SLOW_QUERY_STATS['total_queries'] += 1
    _SLOW_QUERY_STATS['total_query_time_ms'] += duration_ms

    if duration_ms >= _VERY_SLOW_QUERY_THRESHOLD_MS:
        _SLOW_QUERY_STATS['very_slow_queries'] += 1
        _SLOW_QUERY_STATS['slow_queries'] += 1
        _SLOW_QUERY_STATS['last_slow_query'] = query_name
        _SLOW_QUERY_STATS['last_slow_query_time'] = datetime.now(TAIPEI_TZ).isoformat()
    elif duration_ms >= _SLOW_QUERY_THRESHOLD_MS:
        _SLOW_QUERY_STATS['slow_queries'] += 1
        _SLOW_QUERY_STATS['last_slow_query'] = query_name
        _SLOW_QUERY_STATS['last_slow_query_time'] = datetime.now(TAIPEI_TZ).isoformat()


# ==========================================
# 輔助函數
# ==========================================

def get_color_for_string(s):
    """為字串生成一個穩定且美觀的 HSL 顏色"""
    if not s:
        return "hsl(0, 0%, 85%)"
    hash_val = int(hashlib.md5(s.encode('utf-8'), usedforsecurity=False).hexdigest(), 16)
    hue = hash_val % 360
    return f"hsl({hue}, 60%, 88%)"


def load_scheduler_stats():
    """讀取排程統計資料"""
    stats_path = os.path.join(BASE_DIR, 'data', 'scheduler_stats.json')
    if os.path.exists(stats_path):
        try:
            with open(stats_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except (IOError, json.JSONDecodeError):
            return {}
    return {}


# ==========================================
# 核心數據函數
# ==========================================

def get_consolidated_data():
    """統一封裝：獲取全分類去重後的當前數據、昨日對比及差值 (帶快取)"""
    global _DASHBOARD_DATA_CACHE

    now = datetime.now(TAIPEI_TZ)

    # V-Opt: 先檢查快取（無需鎖）
    if (_DASHBOARD_DATA_CACHE['consolidated_data'] is not None and
        _DASHBOARD_DATA_CACHE['consolidated_timestamp'] is not None):
        cache_age = (now.timestamp() - _DASHBOARD_DATA_CACHE['consolidated_timestamp'])
        if cache_age < _DASHBOARD_CACHE_TTL:
            sys_log.debug(f"[Dashboard] [Cache] ✅ 使用快取資料 | 快取年齡: {cache_age:.1f}秒")
            return _DASHBOARD_DATA_CACHE['consolidated_data'], _DASHBOARD_DATA_CACHE['today_start']

    # V-Opt: 使用檔案鎖避免多 gunicorn worker 同時重建快取
    # 注意: get_consolidated_data 通常由 get_full_dashboard_data 調用，
    # 後者已持有 _DASHBOARD_FILE_LOCK，因此這裡可以不重複鎖定
    # 但為避免直接調用時的競爭問題，仍保留快取檢查邏輯

    # 再次檢查快取（可能其他 worker 已經更新）
    if (_DASHBOARD_DATA_CACHE['consolidated_data'] is not None and
        _DASHBOARD_DATA_CACHE['consolidated_timestamp'] is not None):
        cache_age = (now.timestamp() - _DASHBOARD_DATA_CACHE['consolidated_timestamp'])
        if cache_age < _DASHBOARD_CACHE_TTL:
            sys_log.debug(f"[Dashboard] [Cache] ✅ 使用快取資料 (其他 worker 已更新) | 快取年齡: {cache_age:.1f}秒")
            return _DASHBOARD_DATA_CACHE['consolidated_data'], _DASHBOARD_DATA_CACHE['today_start']

    sys_log.debug("[Dashboard] [Cache] 🔄 快取過期或不存在，重新查詢資料庫")
    query_start_time = time.time()

    db = DatabaseManager()
    session = db.get_session()
    today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)  # 保持台北時區
    seven_days_ago = today_start - timedelta(days=7)
    thirty_days_ago = today_start - timedelta(days=30)

    try:
        # Query 1: Get the latest price record for every product
        latest_price_subq = session.query(
            func.max(PriceRecord.id).label('max_id')
        ).group_by(PriceRecord.product_id).subquery()

        latest_records = session.query(PriceRecord).options(
            joinedload(PriceRecord.product)
        ).join(latest_price_subq, PriceRecord.id == latest_price_subq.c.max_id).all()

        product_ids = [r.product_id for r in latest_records]
        if not product_ids:
            session.close()
            return [], today_start

        # Query 2: Get yesterday's closing prices for all products
        yesterday_prices_subq = session.query(
            PriceRecord.product_id,
            func.max(PriceRecord.id).label('max_id')
        ).filter(
            PriceRecord.product_id.in_(product_ids),
            PriceRecord.timestamp < today_start
        ).group_by(PriceRecord.product_id).subquery()

        yesterday_prices_q = session.query(
            PriceRecord.product_id, PriceRecord.price
        ).join(
            yesterday_prices_subq,
            PriceRecord.id == yesterday_prices_subq.c.max_id
        )
        yesterday_prices_map = {pid: price for pid, price in yesterday_prices_q}

        # Query 3: Get specific historical price points (7 days ago and 30 days ago)
        def get_price_map_before(target_date):
            subq = session.query(
                PriceRecord.product_id,
                func.max(PriceRecord.timestamp).label('max_ts')
            ).filter(
                PriceRecord.product_id.in_(product_ids),
                PriceRecord.timestamp < target_date
            ).group_by(PriceRecord.product_id).subquery()

            q = session.query(PriceRecord.product_id, PriceRecord.price).join(
                subq,
                and_(PriceRecord.product_id == subq.c.product_id, PriceRecord.timestamp == subq.c.max_ts)
            )
            return {pid: price for pid, price in q}

        prices_7d_ago_map = get_price_map_before(seven_days_ago + timedelta(days=1))
        prices_30d_ago_map = get_price_map_before(thirty_days_ago + timedelta(days=1))

        # Query 4: Get TODAY's records only (for sparkline/intraday change)
        today_records_q = session.query(PriceRecord).filter(
            PriceRecord.product_id.in_(product_ids),
            PriceRecord.timestamp >= today_start
        ).order_by(PriceRecord.product_id, PriceRecord.timestamp).all()

        today_map = {}
        for r in today_records_q:
            if r.product_id not in today_map:
                today_map[r.product_id] = []
            today_map[r.product_id].append(r)

        # Final Assembly
        unique_items = []
        for r in latest_records:
            pid = r.product_id
            product = r.product
            safe_product_url = normalize_momo_product_url(getattr(product, 'url', None), getattr(product, 'i_code', ''))

            price_7d = prices_7d_ago_map.get(pid)
            price_30d = prices_30d_ago_map.get(pid)

            stats_7d_diff = r.price - price_7d if price_7d is not None else 0
            stats_30d_diff = r.price - price_30d if price_30d is not None else 0

            today_records = today_map.get(pid, [])
            today_diff = 0
            today_changes = []
            if len(today_records) > 1:
                today_diff = today_records[-1].price - today_records[0].price

            y_price = yesterday_prices_map.get(pid)
            yesterday_diff = r.price - y_price if y_price is not None else 0

            status = "NONE"
            if yesterday_diff > 0:
                status = "PRICE_UP"
            elif yesterday_diff < 0:
                status = "PRICE_DOWN"

            last_p = y_price if y_price is not None else (today_records[0].price if today_records else r.price)
            for tr in today_records:
                if tr.price != last_p:
                    diff = tr.price - last_p
                    today_changes.append({
                        'time': tr.timestamp.strftime('%H:%M'),
                        'price': tr.price,
                        'diff': diff
                    })
                    last_p = tr.price

            unique_items.append({
                'record': r,
                'safe_product_url': safe_product_url or _build_momo_product_url(getattr(product, 'i_code', '')),
                'stats': {'7d_diff': stats_7d_diff, '30d_diff': stats_30d_diff, '1d_diff': today_diff},
                'yesterday_diff': yesterday_diff,
                'today_changes': today_changes,
                'status': status
            })

        # 更新快取
        _DASHBOARD_DATA_CACHE['consolidated_data'] = unique_items
        _DASHBOARD_DATA_CACHE['consolidated_timestamp'] = now.timestamp()
        _DASHBOARD_DATA_CACHE['today_start'] = today_start

        query_duration_ms = (time.time() - query_start_time) * 1000
        track_query_time('get_consolidated_data', query_duration_ms)
        sys_log.debug(f"[Dashboard] [Cache] 快取已更新 | 商品數: {len(unique_items)} | 耗時: {query_duration_ms:.0f}ms")

        return unique_items, today_start
    finally:
        session.close()


def get_full_dashboard_data(force_rebuild=False):
    """獲取完整的看板資料，包含快取清單與全部 KPIs (深度快取)"""
    global _DASHBOARD_DATA_CACHE
    now = datetime.now(TAIPEI_TZ)

    # V-Opt: 先檢查快取（無需鎖）
    if not force_rebuild and _DASHBOARD_DATA_CACHE.get('full_data') and _DASHBOARD_DATA_CACHE.get('full_timestamp'):
        age = now.timestamp() - _DASHBOARD_DATA_CACHE['full_timestamp']
        if age < _DASHBOARD_CACHE_TTL:
            sys_log.debug(f"[Dashboard] [Cache] ✅ 使用完整看板快取 | 快取年齡: {age:.0f}秒")
            return _DASHBOARD_DATA_CACHE['full_data']

    shared_full_data = None if force_rebuild else _load_shared_full_dashboard_cache(now)
    if shared_full_data:
        return shared_full_data

    # V-Opt: 使用檔案鎖避免多 gunicorn worker 同時計算
    dashboard_lock = _new_dashboard_file_lock()
    lock_acquired = dashboard_lock.acquire(blocking=False)
    if not lock_acquired:
        # 其他 worker 正在重建時，先用舊快取救首屏，避免使用者第一次打開卡住。
        stale_full_data = None if force_rebuild else _load_stale_full_dashboard_cache(now)
        if stale_full_data:
            return stale_full_data

        sys_log.info("[Dashboard] [Cache] ⏳ 等待其他 worker 重建快取...")
        deadline = time.time() + 5
        while time.time() < deadline:
            time.sleep(0.25)
            shared_full_data = None if force_rebuild else _load_shared_full_dashboard_cache(now)
            if shared_full_data:
                return shared_full_data
            dashboard_lock = _new_dashboard_file_lock()
            lock_acquired = dashboard_lock.acquire(blocking=False)
            if lock_acquired:
                break

        shared_full_data = None if force_rebuild else _load_shared_full_dashboard_cache(now)
        if shared_full_data:
            return shared_full_data
        if not lock_acquired:
            stale_full_data = None if force_rebuild else _load_stale_full_dashboard_cache(now)
            if stale_full_data:
                return stale_full_data
            dashboard_lock = _new_dashboard_file_lock()
            lock_acquired = dashboard_lock.acquire(blocking=True)

    try:
        # 再次檢查快取（可能其他 worker 已經更新）
        if not force_rebuild and _DASHBOARD_DATA_CACHE.get('full_data') and _DASHBOARD_DATA_CACHE.get('full_timestamp'):
            age = now.timestamp() - _DASHBOARD_DATA_CACHE['full_timestamp']
            if age < _DASHBOARD_CACHE_TTL:
                sys_log.debug(f"[Dashboard] [Cache] ✅ 使用完整看板快取 (其他 worker 已更新) | 快取年齡: {age:.0f}秒")
                return _DASHBOARD_DATA_CACHE['full_data']

        shared_full_data = None if force_rebuild else _load_shared_full_dashboard_cache(now)
        if shared_full_data:
            return shared_full_data

        sys_log.info("[Dashboard] [Cache] 🔄 完整快取過期，重新計算所有 KPIs 與統計數據...")
        query_start_time = time.time()

        unique_items, today_start = get_consolidated_data()
        today_start_db = today_start  # 保持台北時區

        db = DatabaseManager()
        session = db.get_session()

        try:
            # A. 基礎清單統計
            increase_items = [item for item in unique_items if item['yesterday_diff'] > 0]
            decrease_items = [item for item in unique_items if item['yesterday_diff'] < 0]

            # B. 分類筆數統計
            cat_counts = {}
            for item in unique_items:
                c = item['record'].product.category
                if c:
                    cat_counts[c] = cat_counts.get(c, 0) + 1
            all_categories = [f"{cat} ({count}筆)" for cat, count in sorted(cat_counts.items())]

            # C. 核心 KPI 統計
            total_products_history = session.query(Product).count()
            total_price_records = session.query(PriceRecord).count()
            today_updates = session.query(PriceRecord).filter(PriceRecord.timestamp >= today_start_db).count()

            # 今日新增商品
            new_pids_query = session.query(PriceRecord.product_id).group_by(
                PriceRecord.product_id
            ).having(func.min(PriceRecord.timestamp) >= today_start_db)
            new_product_ids = {r[0] for r in new_pids_query.all()}
            today_new_products = len(new_product_ids)

            # D. 今日下架商品處理
            raw_delisted_items = session.query(Product).filter(
                Product.status == 'INACTIVE',
                Product.updated_at >= today_start_db
            ).all()

            today_delisted_items = []
            if raw_delisted_items:
                delisted_ids = [p.id for p in raw_delisted_items]
                last_prices_subq = session.query(
                    PriceRecord.product_id,
                    func.max(PriceRecord.id).label('max_id')
                ).filter(PriceRecord.product_id.in_(delisted_ids)).group_by(PriceRecord.product_id).subquery()

                last_prices_q = session.query(PriceRecord.product_id, PriceRecord.price).join(
                    last_prices_subq, PriceRecord.id == last_prices_subq.c.max_id).all()
                price_map = {pid: price for pid, price in last_prices_q}

                for p in raw_delisted_items:
                    today_delisted_items.append({'product': p, 'last_price': price_map.get(p.id, 0)})

            # E. 週增長
            week_ago_db = now.replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=7)
            week_new_products = session.query(func.count(Product.id)).filter(
                Product.id.in_(
                    session.query(PriceRecord.product_id)
                    .group_by(PriceRecord.product_id)
                    .having(func.min(PriceRecord.timestamp) >= week_ago_db)
                )
            ).scalar() or 0

            # F. 價格穩定商品數
            try:
                stable_count = session.query(PriceRecord.product_id).filter(
                    PriceRecord.timestamp >= week_ago_db
                ).group_by(PriceRecord.product_id).having(
                    func.count(func.distinct(PriceRecord.price)) == 1
                ).count()
            except Exception:
                stable_count = 0

            # G. 最大變動計算
            max_change_item = None
            max_change_value = 0
            for item in unique_items:
                if abs(item['yesterday_diff']) > abs(max_change_value):
                    max_change_value = item['yesterday_diff']
                    max_change_item = item

            # H. 最活躍分類
            category_activity = {}
            for item in increase_items + decrease_items:
                cat = item['record'].product.category
                if cat:
                    category_activity[cat] = category_activity.get(cat, 0) + 1
            most_active_category_item = max(category_activity.items(), key=lambda x: x[1]) if category_activity else (None, 0)

            # I. 組合結果
            full_data = {
                'unique_items': unique_items,
                'today_start': today_start,
                'today_start_db': today_start_db,
                'increase_items_all': increase_items,
                'decrease_items_all': decrease_items,
                'all_categories': all_categories,
                'new_product_ids': new_product_ids,
                'total_products_history': total_products_history,
                'total_price_records': total_price_records,
                'today_updates': today_updates,
                'today_new_products': today_new_products,
                'today_delisted_count': len(raw_delisted_items),
                'today_delisted_items': today_delisted_items,
                'max_change_item': max_change_item,
                'max_change_value': max_change_value,
                'avg_increase': sum(item['yesterday_diff'] for item in increase_items) / len(increase_items) if increase_items else 0,
                'avg_decrease': sum(item['yesterday_diff'] for item in decrease_items) / len(decrease_items) if decrease_items else 0,
                'activity_rate': (len(increase_items) + len(decrease_items)) / total_products_history * 100 if total_products_history > 0 else 0,
                'active_count': len(increase_items) + len(decrease_items),
                'week_new_products': week_new_products,
                'stable_count': stable_count,
                'most_active_category': most_active_category_item[0],
                'most_active_count': most_active_category_item[1]
            }

            # 更新快取
            _DASHBOARD_DATA_CACHE['full_data'] = full_data
            _DASHBOARD_DATA_CACHE['full_timestamp'] = now.timestamp()
            _write_shared_full_dashboard_cache(full_data)

            query_duration_ms = (time.time() - query_start_time) * 1000
            track_query_time('get_full_dashboard_data', query_duration_ms)
            sys_log.info(f"[Dashboard] [Cache] ✅ 完整看板快取已更新 | 耗時: {query_duration_ms:.0f}ms")

            return full_data
        except Exception as e:
            sys_log.error(f"[Dashboard] KPI 計算失敗: {e}")
            import traceback
            traceback.print_exc()
            return None
        finally:
            session.close()
    finally:
        # V-Opt: 確保釋放檔案鎖
        if lock_acquired:
            dashboard_lock.release()


def get_dashboard_stats():
    """計算看板統計數據 (供通知使用) — backward-compat wrapper"""
    from services.dashboard_service import get_dashboard_stats as _get_dashboard_stats
    return _get_dashboard_stats()


# ==========================================
# 頁面路由
# ==========================================

@dashboard_bp.route('/')
@login_required
def index():
    """商品看板首頁"""
    db = DatabaseManager()
    session = db.get_session()
    page = request.args.get('page', 1, type=int)
    category_filter = request.args.get('category', 'all')
    sort_by = request.args.get('sort_by', 'timestamp')
    filter_type = request.args.get('filter', 'all')
    order = request.args.get('order', 'desc')
    search_query = request.args.get('q', '').strip()
    per_page = 50

    now_taipei = datetime.now(TAIPEI_TZ)
    today_start_db = now_taipei.replace(hour=0, minute=0, second=0, microsecond=0)  # 保持台北時區

    try:
        # 使用深度快取獲取所有數據
        data = get_full_dashboard_data()
        if not data:
            return render_template('index.html', error="無法載入數據，請檢查資料庫。")

        unique_items = data['unique_items']
        today_start = data['today_start']
        today_start_db = data['today_start_db']
        increase_items = data['increase_items_all']
        decrease_items = data['decrease_items_all']
        all_categories = data['all_categories']
        new_product_ids = data['new_product_ids']
        total_products_history = data['total_products_history']
        today_new_products = data['today_new_products']
        total_price_records = data['total_price_records']
        today_updates = data['today_updates']
        today_delisted_count = data['today_delisted_count']
        today_delisted_items = data['today_delisted_items']
        max_change_item = data['max_change_item']
        max_change_value = data['max_change_value']
        avg_increase = data['avg_increase']
        avg_decrease = data['avg_decrease']
        activity_rate = data['activity_rate']
        week_new_products = data['week_new_products']
        stable_count = data['stable_count']
        most_active_category = data['most_active_category']
        most_active_count = data['most_active_count']
        active_count = data.get('active_count', 0)

        # 讀取系統狀態
        system_status = {"status": "UNKNOWN", "message": "尚無執行紀錄", "timestamp": "-"}
        status_path = os.path.join(BASE_DIR, 'data/system_status.json')
        if os.path.exists(status_path):
            try:
                with open(status_path, 'r', encoding='utf-8') as f:
                    system_status = json.load(f)
            except:
                pass

        # 後端篩選
        scheduler_stats = load_scheduler_stats()

        # Handle old scheduler stats format
        if scheduler_stats.get('momo_task') and isinstance(scheduler_stats.get('momo_task'), dict):
            scheduler_stats['momo_task'] = [scheduler_stats['momo_task']]
        if scheduler_stats.get('edm_task') and isinstance(scheduler_stats.get('edm_task'), dict):
            scheduler_stats['edm_task'] = [scheduler_stats['edm_task']]

        filtered_items = []
        ai_pick_skus = []
        ai_pick_map = {}
        ai_pick_summary = None
        if filter_type == 'ai_picks':
            ai_pick_skus, ai_pick_map = _load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT)
            ai_pick_summary = _summarize_ai_pick_selection(ai_pick_map)

        # 先處理搜尋
        if search_query:
            search_lower = search_query.lower()
            base_items = [
                item for item in unique_items
                if (item['record'].product.name and search_lower in item['record'].product.name.lower()) or
                   (item['record'].product.i_code and search_lower in str(item['record'].product.i_code))
            ]
        else:
            base_items = unique_items

        # 處理狀態篩選
        if filter_type == 'increase':
            filtered_items = [i for i in base_items if i in increase_items]
        elif filter_type == 'decrease':
            filtered_items = [i for i in base_items if i in decrease_items]
        elif filter_type == 'new':
            filtered_items = [i for i in base_items if i['record'].product_id in new_product_ids]
        elif filter_type == 'ai_picks':
            pick_set = set(ai_pick_skus)
            filtered_items = [
                i for i in base_items
                if str(i['record'].product.i_code) in pick_set
            ]
        elif filter_type == 'delisted':
            for item in today_delisted_items:
                class DelistedRecord:
                    def __init__(self, p, price):
                        self.product = p
                        self.price = price
                        self.timestamp = p.updated_at

                if not search_query or search_query.lower() in item['product'].name.lower():
                    filtered_items.append({
                        'record': DelistedRecord(item['product'], item['last_price']),
                        'stats': {'1d_diff': 0, '7d_diff': 0, '30d_diff': 0},
                        'yesterday_diff': 0,
                        'today_changes': [],
                        'status': 'DELISTED'
                    })
        else:
            if category_filter != 'all':
                real_category = category_filter
                if "(" in category_filter and "筆)" in category_filter:
                    real_category = category_filter.rsplit(" (", 1)[0]
                filtered_items = [item for item in base_items if item['record'].product.category == real_category]
            else:
                filtered_items = base_items

        # 後端排序
        reverse = (order == 'desc')

        def get_sort_key(item):
            def safe_get(value, default=0):
                return default if value is None else value

            if sort_by == 'i_code':
                return int(safe_get(item['record'].product.i_code, 0))
            if sort_by == 'category':
                return safe_get(item['record'].product.category, '')
            if sort_by == 'name':
                return safe_get(item['record'].product.name, '')
            if sort_by == 'price':
                return safe_get(item['record'].price, 0)
            if sort_by == 'today_change':
                return safe_get(item['stats']['1d_diff'], 0)
            if sort_by == 'yesterday_change':
                return safe_get(item['yesterday_diff'], 0)
            if sort_by == 'week_change':
                return safe_get(item['stats']['7d_diff'], 0)
            if filter_type == 'ai_picks':
                sku = str(item['record'].product.i_code)
                return -ai_pick_map.get(sku, {}).get('rank', 9999)
            return item['record'].timestamp

        sorted_items = sorted(filtered_items, key=get_sort_key, reverse=reverse)

        # 分頁
        total_items = len(sorted_items)
        total_pages = math.ceil(total_items / per_page)

        start_idx = (page - 1) * per_page
        paged_items = sorted_items[start_idx: start_idx + per_page]

        # 為前端準備安全的 created_at 屬性
        for item in paged_items:
            item['safe_created_at'] = getattr(item['record'].product, 'created_at', None)
            sku = str(item['record'].product.i_code)
            item['ai_pick'] = ai_pick_map.get(sku)
            item['safe_momo_url'] = (
                item.get('safe_product_url')
                or normalize_momo_product_url(item['record'].product.url, sku)
                or _build_momo_product_url(sku)
            )

        # 為當前頁面項目添加顏色
        for item in paged_items:
            category_name = item['record'].product.category
            item['category_color'] = get_color_for_string(category_name)

        pchome_map = _load_pchome_competitor_map(
            session,
            [item['record'].product.i_code for item in paged_items]
        )
        for item in paged_items:
            product = item['record'].product
            competitor = pchome_map.get(str(product.i_code))
            item['pchome_competitor'] = competitor
            item['competitor_decision'] = _build_competitor_decision(
                item['record'].price,
                competitor.get('price') if competitor else None
            )

        competitor_overview = _load_competitor_decision_overview(session, unique_items)
        template_name = 'dashboard.html' if request.args.get('ui') == 'legacy' else 'dashboard_v2.html'

        return render_template(template_name,
                               total_products=total_products_history,
                               today_new_products=today_new_products,
                               total_price_records=total_price_records,
                               cnt_increase=len(increase_items),
                               cnt_decrease=len(decrease_items),
                               today_delisted_count=today_delisted_count,
                               today_delisted_items=today_delisted_items,
                               system_status=system_status,
                               items=paged_items,
                               categories=all_categories,
                               current_page=page,
                               total_pages=total_pages,
                               total_items=total_items,
                               datetime_now=now_taipei.strftime('%Y-%m-%d %H:%M:%S'),
                               today_date=now_taipei.strftime('%Y-%m-%d'),
                               public_url=public_url,
                               current_category=category_filter,
                               current_filter=filter_type,
                               search_query=search_query,
                               current_sort=sort_by,
                               current_order=order,
                               ai_pick_summary=ai_pick_summary,
                               scheduler_stats=scheduler_stats,
                               avg_increase=avg_increase,
                               avg_decrease=avg_decrease,
                               activity_rate=activity_rate,
                               active_count=active_count,
                               max_change_item=max_change_item,
                               max_change_value=max_change_value,
                               week_new_products=week_new_products,
                               stable_count=stable_count,
                               most_active_category=most_active_category,
                               most_active_count=most_active_count,
                               competitor_overview=competitor_overview,
                               ai_pick_list_limit=PRODUCT_PICK_LIST_LIMIT,
                               active_page='dashboard')
    except Exception as e:
        sys_log.error(f"[Web] [Dashboard] 渲染錯誤 | Error: {e}")
        return f"系統維護中，錯誤詳情：{e}"
    finally:
        session.close()
