#!/usr/bin/env python3
"""
后台定时任务 - 数据采集器

按策略定时请求 NIO API 并存储到数据库：
- 工作日 9:00-19:00 采集，10:00-19:00 通知
- 其他时间段：每 1 小时
"""

import os
import time
import logging
import threading
from datetime import datetime, timedelta
from pathlib import Path

from dotenv import load_dotenv

# 加载环境变量
env_path = Path(__file__).parent / ".env"
load_dotenv(env_path)

# ==================== 日志配置 ====================

logger = logging.getLogger('charger.scheduler')
logger.setLevel(logging.INFO)

if not logger.handlers:
    handler = logging.StreamHandler()
    handler.setFormatter(logging.Formatter(
        '[%(asctime)s] [%(levelname)s] %(message)s',
        datefmt='%H:%M:%S'
    ))
    logger.addHandler(handler)


# ==================== 配置 ====================

SCHEDULE_CONFIG = {
    # 工作时间配置
    "work_start_hour": 9,
    "work_end_hour": 19,
    "workday_interval": 60,      # 工作时间段：60秒
    "non_work_interval": 3600,   # 非工作时间段：1小时
}


# ==================== 时间策略 ====================

def is_workday() -> bool:
    """判断是否是工作日（周一至周五）"""
    return datetime.now().weekday() < 5  # 0-4 = 周一至周五


def is_work_hours() -> bool:
    """判断是否是工作时间"""
    hour = datetime.now().hour
    return SCHEDULE_CONFIG["work_start_hour"] <= hour < SCHEDULE_CONFIG["work_end_hour"]


def get_next_interval() -> int:
    """获取下次采集间隔（秒）"""
    if is_workday() and is_work_hours():
        return SCHEDULE_CONFIG["workday_interval"]
    else:
        return SCHEDULE_CONFIG["non_work_interval"]


def get_next_run_desc() -> str:
    """获取下次运行的描述"""
    interval = get_next_interval()
    if interval == 60:
        return "1 分钟后"
    else:
        return "1 小时后"


# ==================== 数据采集 ====================

def fetch_and_save():
    """
    请求多厂商 API 并保存数据

    当前支持的厂商：
    - NIO (蔚来)
    - Gongniu (公牛) - 仅 DC 桩
    """
    from db import Database, init_db, HistoryDB, StateCacheDB
    from token_manager import TokenManager

    station_id = os.environ.get("STATION_ID", "")
    # token 配置: 兼容两种方式
    # 1. 只配 NIO_AUTHORIZATION 环境变量（直接放 token）
    # 2. 通过 python token_manager.py login 登录后持久化到数据库（含 refresh_token，可自动刷新）
    authorization = os.environ.get("NIO_AUTHORIZATION", "")

    # 初始化数据库
    init_db()

    all_connectors = []

    # ========== NIO 数据采集 ==========
    try:
        token_mgr = TokenManager(authorization=authorization or None)
        result = token_mgr.get_chargers(station_id)

        if result.get("result_code") == "success" and result.get("data"):
            raw_data = result["data"]

            # 获取状态缓存
            free_cache = StateCacheDB.get_free_cache()
            charging_cache = StateCacheDB.get_charging_cache()
            occupied_cache = StateCacheDB.get_occupied_cache()

            for c in raw_data.get("connector_detail_list", []):
                state = c.get("state", {})
                state_type = state.get("type", "unknown")
                # 状态映射
                if state_type == "occupy":
                    state_type = "occupied"
                elif state_type == "soc":
                    state_type = "charging"

                connector_key = c.get("connector_name") or c.get("connector_id")

                # 计算各状态时长
                idle_seconds = 0
                charging_seconds = 0
                occupied_seconds = 0

                if state_type == "free":
                    if connector_key in free_cache:
                        started = free_cache[connector_key]
                        if isinstance(started, datetime):
                            idle_seconds = int((datetime.now() - started).total_seconds())
                elif state_type == "charging":
                    if connector_key in charging_cache:
                        started = charging_cache[connector_key]
                        if isinstance(started, datetime):
                            charging_seconds = int((datetime.now() - started).total_seconds())
                elif state_type == "occupied":
                    if connector_key in occupied_cache:
                        started = occupied_cache[connector_key]
                        if isinstance(started, datetime):
                            occupied_seconds = int((datetime.now() - started).total_seconds())

                connector = {
                    "connector_id": c.get("connector_id", ""),
                    "connector_name": c.get("connector_name", ""),
                    "type": c.get("type", "AC"),
                    "power": float(c.get("power", 0)),
                    "state": {
                        "type": state_type,
                        "desc": state.get("desc", "")
                    },
                    "idle_seconds": idle_seconds,
                    "charging_seconds": charging_seconds,
                    "occupied_seconds": occupied_seconds,
                    "vendor": "nio",  # 标识厂商
                }

                # DC 桩特殊字段
                if c.get("type") == "DC":
                    if c.get("estimated_charging_ready_time"):
                        connector["estimated_charging_ready_time"] = c.get("estimated_charging_ready_time")
                    if state_type == "charging" and state.get("desc"):
                        try:
                            connector["charging_progress"] = float(state.get("desc"))
                        except (ValueError, TypeError):
                            pass

                all_connectors.append(connector)

            logger.info(f"NIO 采集成功: {len(all_connectors)} 个充电桩")
        else:
            logger.error(f"NIO API 返回异常: {result.get('result_code')}")

    except Exception as e:
        logger.error(f"NIO 采集失败: {e}")

    # ========== 公牛数据采集 ==========
    try:
        from gongniu_api import GongniuAPI
        gongniu_api = GongniuAPI()
        gongniu_result = gongniu_api.fetch_connectors(dc_only=False)

        if gongniu_result.get("success"):
            gongniu_connectors = gongniu_result["connectors"]

            # 从状态缓存计算公牛桩时长（与 NIO 桩逻辑一致）
            free_cache = StateCacheDB.get_free_cache()
            charging_cache = StateCacheDB.get_charging_cache()
            occupied_cache = StateCacheDB.get_occupied_cache()

            for c in gongniu_connectors:
                state_type = c.get("state", {}).get("type", "unknown")
                connector_key = c.get("connector_name") or c.get("connector_id")

                idle_seconds = 0
                charging_seconds = 0
                occupied_seconds = 0

                if state_type == "free":
                    if connector_key in free_cache:
                        started = free_cache[connector_key]
                        if isinstance(started, datetime):
                            idle_seconds = int((datetime.now() - started).total_seconds())
                elif state_type == "charging":
                    if connector_key in charging_cache:
                        started = charging_cache[connector_key]
                        if isinstance(started, datetime):
                            charging_seconds = int((datetime.now() - started).total_seconds())
                elif state_type == "occupied":
                    if connector_key in occupied_cache:
                        started = occupied_cache[connector_key]
                        if isinstance(started, datetime):
                            occupied_seconds = int((datetime.now() - started).total_seconds())

                c["idle_seconds"] = idle_seconds
                c["charging_seconds"] = charging_seconds
                c["occupied_seconds"] = occupied_seconds

            all_connectors.extend(gongniu_connectors)
            dc_count = sum(1 for c in gongniu_connectors if c["type"] == "DC")
            ac_count = len(gongniu_connectors) - dc_count
            logger.info(f"公牛采集成功: {dc_count} 个 DC 充电桩, {ac_count} 个 AC 充电桩")
        else:
            logger.warning(f"公牛采集失败: {gongniu_result.get('error', '未知错误')}")

    except Exception as e:
        logger.error(f"公牛采集异常: {e}")

    # ========== 保存数据 ==========
    if not all_connectors:
        logger.warning("无充电桩数据，跳过保存")
        return False

    try:
        # 更新状态缓存
        StateCacheDB.update_states(all_connectors)

        # 保存历史数据
        fetch_time = datetime.now()
        saved = HistoryDB.save_status(station_id, all_connectors, fetch_time)

        # 更新最后采集时间
        update_last_fetch_time(fetch_time)

        # 检查订阅触发：邮件发送慢时不阻塞本轮状态保存
        notify_thread = threading.Thread(
            target=check_and_notify_subscribers,
            args=(all_connectors,),
            daemon=True
        )
        notify_thread.start()

        logger.info(f"采集成功: {len(all_connectors)} 个充电桩, 保存 {saved} 条记录")
        return True

    except Exception as e:
        import traceback
        error_trace = traceback.format_exc()
        logger.error(f"采集失败: {e}\n{error_trace}")

        # 记录失败状态
        try:
            from db import Database
            Database.execute(
                """INSERT INTO system_config (`key`, value, updated_at)
                   VALUES ('last_fetch_error', %s, NOW())
                   ON DUPLICATE KEY UPDATE value = %s, updated_at = NOW()""",
                (str(e), str(e))
            )
        except Exception:
            pass

        # 发送告警邮件
        send_alert_email(str(e), error_trace)

        return False


# ==================== 告警通知 ====================

ALERT_EMAIL = os.environ.get("ALERT_EMAIL", "")
_last_alert_time = None  # 上次告警时间
ALERT_COOLDOWN = 3600  # 告警冷却时间（秒），1小时内不重复发送


def send_alert_email(error_msg: str, error_trace: str):
    """
    发送采集失败告警邮件（带冷却时间，防止邮件轰炸）

    Args:
        error_msg: 错误信息
        error_trace: 完整堆栈
    """
    global _last_alert_time

    # 检查冷却时间
    now = datetime.now()
    if _last_alert_time:
        elapsed = (now - _last_alert_time).total_seconds()
        if elapsed < ALERT_COOLDOWN:
            logger.warning(f"告警冷却中，跳过发送（距上次 {int(elapsed)}秒）")
            return

    import smtplib
    import traceback
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart

    smtp_host = os.environ.get("SMTP_HOST", "")
    smtp_port = int(os.environ.get("SMTP_PORT", 465))
    smtp_user = os.environ.get("SMTP_USER", "")
    smtp_password = os.environ.get("SMTP_PASSWORD", "")
    email_from = os.environ.get("EMAIL_FROM", smtp_user)

    # 如果没有配置邮件服务，只记录日志
    if not smtp_host or not smtp_user:
        logger.warning(f"邮件服务未配置，跳过告警邮件发送")
        return

    try:
        # 构建邮件内容
        now_str = now.strftime("%Y-%m-%d %H:%M:%S")
        subject = f"【告警】充电桩数据采集失败 - {now_str}"

        body = f"""
充电桩监控系统数据采集失败！

时间：{now_str}

错误信息：
{error_msg}

详细堆栈：
{error_trace}

请及时检查：
1. NIO API 是否正常
2. 网络连接是否正常
3. authorization token 是否过期

---
此邮件由充电桩监控系统自动发送
        """.strip()

        msg = MIMEMultipart()
        msg["From"] = email_from
        msg["To"] = ALERT_EMAIL
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain", "utf-8"))

        # 根据端口选择连接方式
        if smtp_port == 465:
            with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as server:
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [ALERT_EMAIL], msg.as_string())
        else:
            with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
                server.ehlo()
                if smtp_port == 587:
                    server.starttls()
                    server.ehlo()
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [ALERT_EMAIL], msg.as_string())

        _last_alert_time = now  # 更新上次告警时间
        logger.info(f"告警邮件已发送至 {ALERT_EMAIL}")

    except Exception as e:
        send_trace = traceback.format_exc()
        logger.error(f"告警邮件发送失败: {e}\n{send_trace}")


# ==================== 订阅通知 ====================

# 空闲时间阈值（秒）：只有空闲超过此时间才触发通知，防止刚空闲就被占用
IDLE_THRESHOLD = 60  # 1 分钟

# 功率别名配置：数据库保存 7kW，前端和邮件显示 5kW
POWER_ALIAS = {
    "1号枪": "5",
    "4号桩": "5",
    "14号枪": "5",
}


def get_display_power(connector_name: str, original_power: float) -> str:
    """
    获取显示功率（应用别名）

    Args:
        connector_name: 充电桩名称
        original_power: 原始功率

    Returns:
        显示功率字符串
    """
    return POWER_ALIAS.get(connector_name, str(int(original_power)))


def subscription_matches_connector(subscription: dict, connector: dict) -> bool:
    """判断订阅是否匹配某个空闲桩。"""
    charger_type = subscription.get("charger_type", "all")
    vendor = subscription.get("vendor") or "all"

    return (
        charger_type in ("all", connector.get("type"))
        and vendor in ("all", connector.get("vendor", "nio"))
    )


def send_test_email(to_email: str) -> bool:
    """
    发送测试邮件，验证 SMTP 配置

    Args:
        to_email: 收件人邮箱

    Returns:
        是否发送成功
    """
    import smtplib
    import traceback
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart

    smtp_host = os.environ.get("SMTP_HOST", "")
    smtp_port = int(os.environ.get("SMTP_PORT", 465))
    smtp_user = os.environ.get("SMTP_USER", "")
    smtp_password = os.environ.get("SMTP_PASSWORD", "")
    email_from = os.environ.get("EMAIL_FROM", smtp_user)

    # 检查配置
    print(f"\n=== SMTP 配置检查 ===")
    print(f"SMTP_HOST: {smtp_host or '(未设置)'}")
    print(f"SMTP_PORT: {smtp_port}")
    print(f"SMTP_USER: {smtp_user or '(未设置)'}")
    print(f"SMTP_PASSWORD: {'***已设置***' if smtp_password else '(未设置)'}")
    print(f"EMAIL_FROM: {email_from or '(未设置)'}")

    if not smtp_host:
        print("\n错误: SMTP_HOST 未配置")
        return False
    if not smtp_user:
        print("\n错误: SMTP_USER 未配置")
        return False
    if not smtp_password:
        print("\n错误: SMTP_PASSWORD 未配置")
        return False

    print(f"\n正在连接 {smtp_host}:{smtp_port}...")

    try:
        # 构建测试邮件
        now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        subject = f"【测试】充电桩监控系统邮件测试 - {now_str}"
        body = f"""
这是一封测试邮件。

发送时间: {now_str}
SMTP 服务器: {smtp_host}:{smtp_port}
发件人: {email_from}

如果您收到此邮件，说明邮件配置正确！

---
此邮件由充电桩监控系统自动发送
        """.strip()

        msg = MIMEMultipart()
        msg["From"] = email_from
        msg["To"] = to_email
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain", "utf-8"))

        # 根据端口选择连接方式
        if smtp_port == 465:
            print("使用 SMTP_SSL 连接...")
            with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as server:
                server.set_debuglevel(1)  # 显示调试信息
                print("登录中...")
                server.login(smtp_user, smtp_password)
                print("发送邮件...")
                server.sendmail(email_from, [to_email], msg.as_string())
        else:
            print("使用 SMTP 连接...")
            with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
                server.set_debuglevel(1)
                server.ehlo()
                if smtp_port == 587:
                    print("启动 TLS...")
                    server.starttls()
                    server.ehlo()
                print("登录中...")
                server.login(smtp_user, smtp_password)
                print("发送邮件...")
                server.sendmail(email_from, [to_email], msg.as_string())

        print(f"\n✓ 邮件发送成功: {to_email}")
        return True

    except Exception as e:
        error_trace = traceback.format_exc()
        print(f"\n✗ 邮件发送失败: {e}")
        print(f"\n详细错误:\n{error_trace}")
        return False


def check_and_notify_subscribers(connectors: list):
    """
    检查并通知订阅用户

    核心逻辑：
    1. 统计空闲桩数量（空闲超过 1 分钟阈值）
    2. 按队列顺序，通知队首 N 人（N = 空闲桩数）
    3. 每人只通知一次
    4. 检查是否有"被抢"情况

    只在工作日 10:00-19:00 发送通知

    Args:
        connectors: 当前充电桩状态列表
    """
    # 只在工作时间发送通知（订阅通知时间：10:00-19:00）
    hour = datetime.now().hour
    if not (is_workday() and 10 <= hour < 19):
        logger.debug("非订阅通知时间，跳过")
        return

    from db import SubscriptionDB

    # 先检查"被抢"情况
    check_grabbed_notification(connectors)

    # 统计空闲桩（超过阈值的才算真正空闲），按类型分组
    free_ac_connectors = []
    free_dc_connectors = []
    free_connectors = []

    for c in connectors:
        if c.get("state", {}).get("type") == "free" and c.get("idle_seconds", 0) >= IDLE_THRESHOLD:
            connector_info = {
                "name": c.get("connector_name", c.get("connector_id", "未知")),
                "idle_seconds": c.get("idle_seconds", 0),
                "power": c.get("power", 7),
                "type": c.get("type", "AC"),
                "vendor": c.get("vendor", "nio"),
            }
            free_connectors.append(connector_info)
            if c.get("type") == "AC":
                free_ac_connectors.append(connector_info)
            else:
                free_dc_connectors.append(connector_info)

    free_ac = len(free_ac_connectors)
    free_dc = len(free_dc_connectors)
    free_all = free_ac + free_dc

    if free_all == 0:
        return  # 无空闲，不触发

    # 获取未通知的订阅队列（VIP 在前）
    queue = SubscriptionDB.get_unnotified_queue()
    if not queue:
        return

    used_connector_indexes = set()
    for i, sub in enumerate(queue):
        if len(used_connector_indexes) >= free_all:
            break

        email = sub["email"]
        charger_type = sub.get("charger_type", "all")
        vendor = sub.get("vendor") or "all"

        # 检查订阅类型和厂商是否匹配当前空闲桩
        matched_index = next(
            (
                idx for idx, connector in enumerate(free_connectors)
                if idx not in used_connector_indexes and subscription_matches_connector(sub, connector)
            ),
            None
        )

        if matched_index is not None:
            # 发送邮件通知
            if send_notification_email(email, charger_type, free_ac_connectors, free_dc_connectors,
                                       i + 1, len(queue), vendor):
                # 标记已通知并过期（每人只通知一次）
                SubscriptionDB.mark_notified(email)
                used_connector_indexes.add(matched_index)
                logger.info(f"已通知 {email}: 排队第 {i + 1} 位（订阅已过期）")


def format_idle_time(seconds: int) -> str:
    """
    格式化空闲时长为可读文本

    Args:
        seconds: 空闲秒数

    Returns:
        格式化后的时长文本，如 "5 分钟"、"1 小时 30 分钟"
    """
    if seconds <= 0:
        return "刚刚"

    minutes = seconds // 60
    if minutes < 1:
        return "刚刚"
    if minutes < 60:
        return f"{minutes} 分钟"

    hours = minutes // 60
    remain_mins = minutes % 60

    if remain_mins == 0:
        return f"{hours} 小时"
    return f"{hours} 小时 {remain_mins} 分钟"


def send_notification_email(email: str, charger_type: str, free_ac_connectors: list, free_dc_connectors: list,
                           queue_position: int = 1, queue_total: int = 1, vendor: str = "all") -> bool:
    """
    发送通知邮件（包含具体的空闲桩信息和空闲时长）

    Args:
        email: 收件人邮箱
        charger_type: 订阅的充电桩类型 (AC/DC/all)
        free_ac_connectors: AC 空闲桩列表
        free_dc_connectors: DC 空闲桩列表
        queue_position: 当前排队位置
        queue_total: 队列总人数
        vendor: 订阅厂商 (all/nio/gongniu)

    Returns:
        是否发送成功
    """
    import smtplib
    import traceback
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart

    smtp_host = os.environ.get("SMTP_HOST", "")
    smtp_port = int(os.environ.get("SMTP_PORT", 465))
    smtp_user = os.environ.get("SMTP_USER", "")
    smtp_password = os.environ.get("SMTP_PASSWORD", "")
    email_from = os.environ.get("EMAIL_FROM", smtp_user)

    # 如果没有配置邮件服务，只记录日志
    if not smtp_host or not smtp_user:
        logger.warning(f"邮件服务未配置，跳过发送: {email}")
        return True  # 返回 True 以便标记已触发

    try:
        # 构建空闲桩详情
        details_lines = []

        def filter_by_vendor(items):
            if vendor == "all":
                return items
            return [c for c in items if c.get("vendor", "nio") == vendor]

        visible_dc_connectors = filter_by_vendor(free_dc_connectors)
        visible_ac_connectors = filter_by_vendor(free_ac_connectors)

        # 根据订阅类型和厂商显示对应的空闲桩
        if charger_type in ["DC", "all"] and visible_dc_connectors:
            details_lines.append("【DC 快充】")
            for c in visible_dc_connectors:
                idle_time = format_idle_time(c["idle_seconds"])
                display_power = get_display_power(c["name"], c["power"])
                details_lines.append(f"  • {c['name']} ({display_power}kW) - 空闲 {idle_time}")
            details_lines.append("")

        if charger_type in ["AC", "all"] and visible_ac_connectors:
            details_lines.append("【AC 慢充】")
            for c in visible_ac_connectors:
                idle_time = format_idle_time(c["idle_seconds"])
                display_power = get_display_power(c["name"], c["power"])
                details_lines.append(f"  • {c['name']} ({display_power}kW) - 空闲 {idle_time}")
            details_lines.append("")

        details_text = "\n".join(details_lines)

        # 统计数量
        free_ac = len(visible_ac_connectors)
        free_dc = len(visible_dc_connectors)
        free_all = free_ac + free_dc

        # 排队提示
        queue_hint = ""
        if queue_total > 1:
            queue_hint = f"\n排队信息：您是第 {queue_position} 位（共 {queue_total} 人排队）\n"

        vendor_text = {"all": "全部品牌", "nio": "蔚来", "gongniu": "公牛"}.get(vendor, "全部品牌")
        charger_type_text = {"all": "任意充电桩", "AC": "AC 慢充", "DC": "DC 快充"}.get(charger_type, "任意充电桩")

        # 构建邮件内容
        subject = f"【充电桩空闲】{free_all} 个空闲桩位，快来充电！"
        body = f"""
您好！

您订阅的范围：{vendor_text} · {charger_type_text}

充电站有空闲桩位：

{details_text}当前状态：
- AC 慢充：{free_ac} 个空闲
- DC 快充：{free_dc} 个空闲
{queue_hint}
⚠️ 重要提醒：
请在 5 分钟内前往充电，否则通知将发送给下一位排队同事。

---
此邮件由充电桩监控系统自动发送
        """.strip()

        msg = MIMEMultipart()
        msg["From"] = email_from
        msg["To"] = email
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain", "utf-8"))

        # 根据端口选择连接方式
        # 465 端口使用 SMTP_SSL，587/25 端口使用 SMTP + STARTTLS
        if smtp_port == 465:
            with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as server:
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [email], msg.as_string())
        else:
            with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
                server.ehlo()
                if smtp_port == 587:
                    server.starttls()
                    server.ehlo()
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [email], msg.as_string())

        logger.info(f"邮件发送成功: {email}")
        return True

    except Exception as e:
        error_trace = traceback.format_exc()
        logger.error(f"邮件发送失败 {email}: {e}\n{error_trace}")
        return False


def check_grabbed_notification(connectors: list):
    """
    检查是否有订阅者的桩被抢了

    逻辑：如果用户收到通知后 5 分钟内，对应类型的充电桩被占用，发送"被抢"邮件

    Args:
        connectors: 当前充电桩状态列表
    """
    from db import SubscriptionDB

    # 获取最近 5 分钟内被通知的订阅者
    recent_notified = SubscriptionDB.get_recent_notified(minutes=5)

    if not recent_notified:
        return

    free_connectors = [
        {
            "type": c.get("type", "AC"),
            "vendor": c.get("vendor", "nio"),
        }
        for c in connectors
        if c.get("state", {}).get("type") == "free"
    ]

    for sub in recent_notified:
        email = sub["email"]

        # 检查对应类型和厂商的桩是否还有空闲
        has_free = any(subscription_matches_connector(sub, c) for c in free_connectors)

        if not has_free:
            # 发送"被抢"邮件
            send_grabbed_email(email, sub.get("charger_type", "all"), sub.get("vendor") or "all")
            # 标记订阅结束
            SubscriptionDB.mark_expired(email)
            logger.info(f"桩被抢了，已通知 {email}")


def send_grabbed_email(email: str, charger_type: str, vendor: str = "all") -> bool:
    """
    发送"被抢"提醒邮件

    Args:
        email: 收件人邮箱
        charger_type: 订阅的充电桩类型
        vendor: 订阅厂商

    Returns:
        是否发送成功
    """
    import smtplib
    import traceback
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart

    smtp_host = os.environ.get("SMTP_HOST", "")
    smtp_port = int(os.environ.get("SMTP_PORT", 465))
    smtp_user = os.environ.get("SMTP_USER", "")
    smtp_password = os.environ.get("SMTP_PASSWORD", "")
    email_from = os.environ.get("EMAIL_FROM", smtp_user)

    if not smtp_host or not smtp_user:
        logger.warning(f"邮件服务未配置，跳过发送: {email}")
        return True

    try:
        type_text = {"AC": "AC 慢充", "DC": "DC 快充", "all": "充电桩"}.get(charger_type, "充电桩")
        vendor_text = {"all": "", "nio": "蔚来", "gongniu": "公牛"}.get(vendor, "")
        target_text = f"{vendor_text}{type_text}"

        subject = f"【提醒】您订阅的{target_text}已被占用"
        body = f"""
您好！

很抱歉通知您，您订阅的{target_text}已被其他同事占用。

建议您继续等待，如果有空闲桩位会再次通知您。

---
此邮件由充电桩监控系统自动发送
        """.strip()

        msg = MIMEMultipart()
        msg["From"] = email_from
        msg["To"] = email
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain", "utf-8"))

        if smtp_port == 465:
            with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as server:
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [email], msg.as_string())
        else:
            with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
                server.ehlo()
                if smtp_port == 587:
                    server.starttls()
                    server.ehlo()
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [email], msg.as_string())

        logger.info(f"被抢邮件发送成功: {email}")
        return True

    except Exception as e:
        error_trace = traceback.format_exc()
        logger.error(f"被抢邮件发送失败 {email}: {e}\n{error_trace}")
        return False


# ==================== 最后采集时间 ====================

LAST_FETCH_KEY = "last_fetch_time"

def get_last_fetch_time() -> datetime:
    """获取最后采集时间"""
    from db import Database
    try:
        row = Database.execute(
            "SELECT value FROM system_config WHERE `key` = %s",
            (LAST_FETCH_KEY,),
            fetch_one=True
        )
        if row and row.get("value"):
            return datetime.fromisoformat(row["value"])
    except Exception as e:
        logger.warning(f"获取最后采集时间失败: {e}")
    return None


def update_last_fetch_time(fetch_time: datetime):
    """更新最后采集时间"""
    from db import Database
    try:
        Database.execute(
            """INSERT INTO system_config (`key`, value, updated_at)
               VALUES (%s, %s, NOW())
               ON DUPLICATE KEY UPDATE value = %s, updated_at = NOW()""",
            (LAST_FETCH_KEY, fetch_time.isoformat(), fetch_time.isoformat())
        )
    except Exception as e:
        logger.warning(f"更新采集时间失败: {e}")


# ==================== 定时任务 ====================

_scheduler_thread = None
_stop_event = threading.Event()
_last_cleanup_check = None  # 上次清理检查日期


def send_expiry_notification(email: str) -> bool:
    """
    发送订阅到期通知邮件

    Args:
        email: 收件人邮箱

    Returns:
        是否发送成功
    """
    import smtplib
    import traceback
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart

    smtp_host = os.environ.get("SMTP_HOST", "")
    smtp_port = int(os.environ.get("SMTP_PORT", 465))
    smtp_user = os.environ.get("SMTP_USER", "")
    smtp_password = os.environ.get("SMTP_PASSWORD", "")
    email_from = os.environ.get("EMAIL_FROM", smtp_user)

    # 如果没有配置邮件服务，只记录日志
    if not smtp_host or not smtp_user:
        logger.warning(f"邮件服务未配置，跳过发送: {email}")
        return True

    try:
        # 计算下周一日期
        today = datetime.now()
        days_until_monday = (7 - today.weekday()) % 7
        if days_until_monday == 0:
            days_until_monday = 7
        next_monday = today + timedelta(days=days_until_monday)
        next_monday_str = next_monday.strftime("%Y-%m-%d")

        subject = "【订阅到期】本周订阅已到期，下周请重新订阅"
        body = f"""
您好！

您的充电桩空闲提醒订阅已到期。

本周订阅将于周五结束，如需继续接收提醒，请在下周一重新订阅。

下周一开始时间：{next_monday_str}

订阅方式：
1. 访问充电桩大盘页面
2. 点击底部「订阅提醒」按钮
3. 填写邮箱并选择充电桩类型

感谢您的使用！

---
此邮件由充电桩监控系统自动发送
        """.strip()

        msg = MIMEMultipart()
        msg["From"] = email_from
        msg["To"] = email
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain", "utf-8"))

        # 根据端口选择连接方式
        if smtp_port == 465:
            with smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) as server:
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [email], msg.as_string())
        else:
            with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
                server.ehlo()
                if smtp_port == 587:
                    server.starttls()
                    server.ehlo()
                server.login(smtp_user, smtp_password)
                server.sendmail(email_from, [email], msg.as_string())

        logger.info(f"到期通知邮件发送成功: {email}")
        return True

    except Exception as e:
        error_trace = traceback.format_exc()
        logger.error(f"到期通知邮件发送失败 {email}: {e}\n{error_trace}")
        return False


def check_weekly_cleanup():
    """
    检查是否需要每周清理订阅（周五 19:00）

    清理前会给所有订阅用户发送到期通知
    """
    global _last_cleanup_check

    now = datetime.now()
    today = now.date()

    # 检查是否是周五且过了 19:00
    if now.weekday() == 4 and now.hour >= 19:  # 周五 19:00 后
        if _last_cleanup_check != today:
            logger.info("周五 19:00 执行每周订阅清理")

            from db import SubscriptionDB

            # 获取所有有效订阅用户，发送到期通知
            subscriptions = SubscriptionDB.get_active_for_week()
            if subscriptions:
                logger.info(f"发送订阅到期通知给 {len(subscriptions)} 个用户")
                for sub in subscriptions:
                    email = sub.get("email")
                    if email:
                        send_expiry_notification(email)

            # 清理所有订阅
            SubscriptionDB.cleanup_weekly()
            _last_cleanup_check = today


def scheduler_loop():
    """定时任务主循环"""
    logger.info("数据采集器启动")
    logger.info(f"策略: 工作日 10:00-19:00 每 {SCHEDULE_CONFIG['workday_interval']}秒")
    logger.info(f"      其他时间 每 {SCHEDULE_CONFIG['non_work_interval']}秒")

    # 首次等待一小段时间再开始（避免启动后立即执行）
    logger.info("5 秒后开始首次采集...")
    _stop_event.wait(5)

    while not _stop_event.is_set():
        try:
            # 检查每周清理
            check_weekly_cleanup()

            # 执行采集
            fetch_and_save()

            # 计算下次间隔
            interval = get_next_interval()
            logger.info(f"下次采集: {get_next_run_desc()}")

            # 等待
            _stop_event.wait(interval)

        except Exception as e:
            logger.error(f"调度异常: {e}")
            _stop_event.wait(60)  # 出错后等待1分钟重试


def start_scheduler():
    """启动定时任务"""
    global _scheduler_thread, _stop_event

    if _scheduler_thread and _scheduler_thread.is_alive():
        logger.warning("定时任务已在运行")
        return

    _stop_event.clear()
    _scheduler_thread = threading.Thread(target=scheduler_loop, daemon=True)
    _scheduler_thread.start()


def stop_scheduler():
    """停止定时任务"""
    global _stop_event
    _stop_event.set()
    logger.info("定时任务已停止")


# ==================== 命令行 ====================

if __name__ == "__main__":
    import sys

    print("=" * 50)
    print("充电桩数据采集器")
    print("=" * 50)

    # 初始化数据库（确保表存在）
    from db import init_db, Database
    init_db()

    # 创建 system_config 表
    with Database.get_connection() as conn:
        with conn.cursor() as cursor:
            cursor.execute("""
                CREATE TABLE IF NOT EXISTS system_config (
                    `key` VARCHAR(64) PRIMARY KEY,
                    `value` TEXT,
                    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
            """)

    if len(sys.argv) > 1:
        cmd = sys.argv[1]

        if cmd == "once":
            # 单次采集
            print("执行单次采集...")
            success = fetch_and_save()
            print("采集成功" if success else "采集失败")

        elif cmd == "start":
            # 启动定时任务
            print("启动定时任务...")
            start_scheduler()

            try:
                while True:
                    time.sleep(1)
            except KeyboardInterrupt:
                print("\n停止...")
                stop_scheduler()

        elif cmd == "status":
            # 查看状态
            last_time = get_last_fetch_time()
            if last_time:
                elapsed = datetime.now() - last_time
                print(f"最后采集时间: {last_time.strftime('%Y-%m-%d %H:%M:%S')}")
                print(f"距今: {int(elapsed.total_seconds() // 60)} 分钟前")
            else:
                print("暂无采集记录")

            print(f"当前策略: {'工作时间(1分钟)' if is_workday() and is_work_hours() else '非工作时间(1小时)'}")

        elif cmd == "test-email":
            # 测试邮件发送
            test_email = sys.argv[2] if len(sys.argv) > 2 else os.environ.get("SMTP_USER", "")
            if not test_email:
                print("用法: python scheduler.py test-email <邮箱地址>")
                print("或者配置 SMTP_USER 环境变量")
                sys.exit(1)

            print(f"发送测试邮件到: {test_email}")
            success = send_test_email(test_email)
            print("邮件发送成功!" if success else "邮件发送失败!")

        else:
            print(f"未知命令: {cmd}")
            print("用法: python scheduler.py [once|start|status|test-email]")

    else:
        print("用法: python scheduler.py <command>")
        print("命令:")
        print("  once       - 执行单次采集")
        print("  start      - 启动定时任务")
        print("  status     - 查看采集状态")
        print("  test-email - 测试邮件发送 (可选: 指定收件人邮箱)")
