#!/usr/bin/env python3
"""
个人日程管理 · 核心调度引擎
============================
功能：多项目任务聚合 → 冲突检测 → 优先级排序 → 自动顺延
"""

import json
import os
from datetime import date, datetime, timedelta
from pathlib import Path
from copy import deepcopy

BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / "data"
PROJECTS_FILE = DATA_DIR / "projects.json"
TASKS_FILE = DATA_DIR / "tasks.json"
PLAN_FILE = DATA_DIR / "daily_plan.json"


def load_json(path):
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def save_json(path, data):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def parse_time(t_str):
    """'09:00' → (9, 0)"""
    h, m = t_str.split(":")
    return int(h), int(m)


def time_to_minutes(t_str):
    h, m = parse_time(t_str)
    return h * 60 + m


def minutes_to_time(m):
    h = m // 60
    mm = m % 60
    return f"{h:02d}:{mm:02d}"


def get_working_range(config):
    start = time_to_minutes(config["working_hours_start"])
    end = time_to_minutes(config["working_hours_end"])
    return start, end


def load_projects():
    """加载项目配置，返回 {project_id: project_info} 映射"""
    data = load_json(PROJECTS_FILE)
    return {p["id"]: p for p in data["projects"]}


def load_tasks():
    """加载全部任务"""
    return load_json(TASKS_FILE)


# ========== 任务聚合 ==========

def aggregate_tasks():
    """
    从各项目空间聚合任务。
    当前版本：从统一的 tasks.json 读取。
    未来可扩展：自动扫描各项目 .workbuddy/memory/ 中的任务追踪文件。
    """
    data = load_tasks()
    tasks = data.get("tasks", [])
    config = data.get("config", {})

    # 只取 pending 状态的任务
    active = [t for t in tasks if t.get("status") == "pending"]
    return active, config


# ========== 冲突检测 ==========

def detect_conflicts(tasks, config):
    """
    检测时间冲突和超载。
    返回 {
        "conflicts": [...],       # 时间重叠冲突
        "overload_days": [...],   # 超载日期
        "warnings": [...]         # 其他警告
    }
    """
    start_min, end_min = get_working_range(config)
    max_daily = config.get("max_daily_hours", 8) * 60
    working_days = set(config.get("working_days", [1, 2, 3, 4, 5]))

    conflicts = []
    overload_days = {}
    warnings = []

    # 按日期分组
    by_date = {}
    for t in tasks:
        sd = t.get("scheduled_date")
        if not sd:
            continue
        by_date.setdefault(sd, []).append(t)

    for d, day_tasks in by_date.items():
        # 排序
        day_tasks.sort(key=lambda x: time_to_minutes(x.get("scheduled_time", "09:00")))

        # 检测重叠
        for i in range(len(day_tasks)):
            a = day_tasks[i]
            a_start = time_to_minutes(a.get("scheduled_time", "09:00"))
            a_end = a_start + a.get("estimated_minutes", 60)

            for j in range(i + 1, len(day_tasks)):
                b = day_tasks[j]
                b_start = time_to_minutes(b.get("scheduled_time", "09:00"))
                b_end = b_start + b.get("estimated_minutes", 60)

                if a_end > b_start:
                    conflicts.append({
                        "type": "time_overlap",
                        "date": d,
                        "task_a": a["id"],
                        "task_a_title": a["title"],
                        "task_a_end": minutes_to_time(a_end),
                        "task_b": b["id"],
                        "task_b_title": b["title"],
                        "task_b_start": b.get("scheduled_time"),
                        "overlap_minutes": a_end - b_start
                    })

        # 检测超载
        total_min = sum(t.get("estimated_minutes", 60) for t in day_tasks)
        if total_min > max_daily:
            overload_days[d] = {
                "total_minutes": total_min,
                "max_minutes": max_daily,
                "over_minutes": total_min - max_daily,
                "task_count": len(day_tasks)
            }

        # 检测非工作日
        try:
            dt = date.fromisoformat(d)
            if dt.weekday() + 1 not in working_days:
                warnings.append({
                    "type": "non_working_day",
                    "date": d,
                    "weekday": dt.strftime("%A"),
                    "task_count": len(day_tasks)
                })
        except ValueError:
            pass

    # 检测 deadline 风险
    today = date.today().isoformat()
    for t in tasks:
        dl = t.get("deadline")
        sd = t.get("scheduled_date")
        if dl and sd and dl < sd:
            warnings.append({
                "type": "deadline_before_schedule",
                "task_id": t["id"],
                "task_title": t["title"],
                "deadline": dl,
                "scheduled": sd
            })

    return {
        "conflicts": conflicts,
        "overload_days": list(overload_days.items()),
        "overload_details": overload_days,
        "warnings": warnings
    }


# ========== 优先级排序 ==========

def sort_by_priority(tasks):
    """
    排序规则：
    1. priority 升序（1最高）
    2. deadline 升序（越近越前）
    3. estimated_minutes 降序（大任务优先）
    """
    def sort_key(t):
        p = t.get("priority", 99)
        dl = t.get("deadline", "9999-12-31")
        est = -(t.get("estimated_minutes", 60))
        return (p, dl, est)

    return sorted(tasks, key=sort_key)


# ========== 调度与顺延 ==========

def schedule_tasks(tasks, config, from_date=None, resolved_choices=None):
    """
    核心调度算法：
    1. 按优先级排序
    2. 逐日分配时间槽
    3. 超载时记录冲突，由用户决策或自动顺延

    resolved_choices: {"t-001": "keep", "t-002": "defer"}  # keep=保留当天, defer=顺延
    """
    if from_date is None:
        from_date = date.today().isoformat()

    start_min, end_min = get_working_range(config)
    max_daily = config.get("max_daily_hours", 8) * 60
    working_days = set(config.get("working_days", [1, 2, 3, 4, 5]))

    sorted_tasks = sort_by_priority(tasks)

    # 如果用户做了选择，先处理
    keep_ids = set()
    defer_ids = set()
    if resolved_choices:
        for tid, choice in resolved_choices.items():
            if choice == "keep":
                keep_ids.add(tid)
            elif choice == "defer":
                defer_ids.add(tid)

    schedule = {}
    current_date = date.fromisoformat(from_date)

    for t in sorted_tasks:
        tid = t["id"]
        est = t.get("estimated_minutes", 60)

        # 如果用户已选择 defer，跳过当天
        if tid in defer_ids:
            current_date = _next_working_day(current_date, working_days)
            t["scheduled_date"] = current_date.isoformat()
            t["scheduled_time"] = config["working_hours_start"]
            t["_auto_deferred"] = True
            defer_ids.discard(tid)

        sd = t.get("scheduled_date", from_date)

        # 推进到第一个可用工作日
        d = date.fromisoformat(sd)
        while d.weekday() + 1 not in working_days:
            d += timedelta(days=1)

        # 分配到日期
        assigned = False
        max_future = d + timedelta(days=60)
        while d <= max_future:
            day_key = d.isoformat()
            schedule.setdefault(day_key, {
                "date": day_key,
                "weekday": d.strftime("%A"),
                "tasks": [],
                "total_minutes": 0,
                "remaining_minutes": max_daily
            })

            day = schedule[day_key]
            if day["total_minutes"] + est <= max_daily:
                # 排时间槽
                if day["tasks"]:
                    last = day["tasks"][-1]
                    last_end = time_to_minutes(last.get("scheduled_time", config["working_hours_start"])) + last.get("estimated_minutes", 60)
                    st = max(last_end, start_min)
                else:
                    st = start_min

                if st + est <= end_min:
                    t["scheduled_date"] = day_key
                    t["scheduled_time"] = minutes_to_time(st)
                    day["tasks"].append(t)
                    day["total_minutes"] += est
                    day["remaining_minutes"] = max_daily - day["total_minutes"]
                    assigned = True
                    break

            d += timedelta(days=1)

        if not assigned:
            t["_schedule_error"] = "无法在60天内安排此任务"

    # 处理被顺延后影响的下游任务（级联调整）
    cascade_adjust(schedule, config, working_days)

    return {
        "schedule": schedule,
        "from_date": from_date,
        "generated_at": datetime.now().isoformat()
    }


def cascade_adjust(schedule, config, working_days):
    """
    级联调整：被顺延的任务之后同一天的其他任务，如果有依赖关系或时间顺序，
    自动向后顺延，保持原有顺序。
    """
    max_daily = config.get("max_daily_hours", 8) * 60
    start_min, end_min = get_working_range(config)

    for day_key in sorted(schedule.keys()):
        day = schedule[day_key]
        day_tasks = day.get("tasks", [])

        if not day_tasks:
            continue

        # 重新计算当日时间槽
        current_slot = start_min
        for t in day_tasks:
            est = t.get("estimated_minutes", 60)
            if current_slot + est > end_min:
                # 溢出，顺延此任务
                t["scheduled_time"] = minutes_to_time(start_min)
                t["_cascaded"] = True
                d = date.fromisoformat(day_key) + timedelta(days=1)
                while d.weekday() + 1 not in working_days:
                    d += timedelta(days=1)
                next_key = d.isoformat()
                schedule.setdefault(next_key, {
                    "date": next_key,
                    "weekday": d.strftime("%A"),
                    "tasks": [],
                    "total_minutes": 0,
                    "remaining_minutes": max_daily
                })
                schedule[next_key]["tasks"].append(t)
                schedule[next_key]["total_minutes"] += est
                schedule[next_key]["remaining_minutes"] = max_daily - schedule[next_key]["total_minutes"]
            else:
                t["scheduled_time"] = minutes_to_time(current_slot)
                current_slot += est

        # 重新计算当日统计
        actual_total = sum(t.get("estimated_minutes", 60) for t in schedule[day_key].get("tasks", []) if not t.get("_cascaded"))
        schedule[day_key]["total_minutes"] = actual_total
        schedule[day_key]["remaining_minutes"] = max_daily - actual_total


# ========== 每日计划生成 ==========

def generate_daily_plan(date_str=None):
    """生成指定日期的日程计划"""
    tasks, config = aggregate_tasks()
    projects = load_projects()

    if date_str is None:
        date_str = date.today().isoformat()

    day_tasks = [t for t in tasks if t.get("scheduled_date") == date_str]
    day_tasks.sort(key=lambda x: time_to_minutes(x.get("scheduled_time", "09:00")))

    # 检测冲突
    conflicts = detect_conflicts(day_tasks, config)

    plan = {
        "date": date_str,
        "weekday": date.fromisoformat(date_str).strftime("%A"),
        "tasks": [],
        "total_tasks": len(day_tasks),
        "total_estimated_minutes": sum(t.get("estimated_minutes", 60) for t in day_tasks),
        "conflicts": conflicts,
        "working_hours": f'{config["working_hours_start"]} - {config["working_hours_end"]}',
        "max_daily_hours": config.get("max_daily_hours", 8)
    }

    for t in day_tasks:
        pid = t.get("project_id", "")
        cat = t.get("category", "")
        proj = projects.get(pid, {"name": "未知项目", "color": "#888780"})
        plan["tasks"].append({
            "id": t["id"],
            "time": t.get("scheduled_time", "--:--"),
            "title": t["title"],
            "description": t.get("description", ""),
            "estimated_minutes": t.get("estimated_minutes", 60),
            "project": proj["name"],
            "project_color": proj["color"],
            "category": cat,
            "priority": t.get("priority", 99),
            "deadline": t.get("deadline"),
            "tags": t.get("tags", []),
            "status": t.get("status", "pending"),
            "source": t.get("source", "project")
        })

    return plan


# ========== 周概览 ==========

def generate_weekly_overview(monday_str=None):
    """生成本周概览"""
    tasks, config = aggregate_tasks()
    projects = load_projects()

    if monday_str is None:
        today = date.today()
        monday = today - timedelta(days=today.weekday())
        monday_str = monday.isoformat()

    monday = date.fromisoformat(monday_str)
    sunday = monday + timedelta(days=6)

    week_days = []
    week_total = 0
    project_breakdown = {}

    for i in range(7):
        d = monday + timedelta(days=i)
        ds = d.isoformat()
        day_tasks = [t for t in tasks if t.get("scheduled_date") == ds]
        day_min = sum(t.get("estimated_minutes", 60) for t in day_tasks)
        week_total += day_min
        week_days.append({
            "date": ds,
            "weekday": d.strftime("%A"),
            "task_count": len(day_tasks),
            "minutes": day_min,
            "hours": round(day_min / 60, 1),
            "tasks": [{
                "id": t["id"],
                "time": t.get("scheduled_time", "--:--"),
                "title": t["title"],
                "project": projects.get(t.get("project_id", ""), {}).get("name", "?"),
                "project_color": projects.get(t.get("project_id", ""), {}).get("color", "#888"),
                "category": t.get("category", ""),
                "estimated_minutes": t.get("estimated_minutes", 60),
                "priority": t.get("priority", 99)
            } for t in day_tasks]
        })

        for t in day_tasks:
            pid = t.get("project_id", "unknown")
            pname = projects.get(pid, {}).get("name", pid)
            project_breakdown.setdefault(pname, {"task_count": 0, "total_minutes": 0, "color": projects.get(pid, {}).get("color", "#888")})
            project_breakdown[pname]["task_count"] += 1
            project_breakdown[pname]["total_minutes"] += t.get("estimated_minutes", 60)

    return {
        "week_start": monday_str,
        "week_end": sunday.isoformat(),
        "days": week_days,
        "total_tasks": sum(d["task_count"] for d in week_days),
        "total_hours": round(week_total / 60, 1),
        "project_breakdown": project_breakdown,
        "generated_at": datetime.now().isoformat()
    }


# ========== 添加任务 ==========

def add_task(task_data):
    """
    添加新任务。支持微信简写格式。
    task_data: {
        "title": "任务标题",
        "project_id": "ai-qifu",
        "estimated_minutes": 60,
        "priority": 2,
        "deadline": "2026-06-30",
        "scheduled_date": "2026-06-25",
        "scheduled_time": "10:00",
        "description": "",
        "source": "manual" | "wechat",
        "source_raw": "微信输入的原文"
    }
    """
    data = load_tasks()

    # 生成 ID
    existing_ids = [int(t["id"].replace("t-", "")) for t in data["tasks"] if t["id"].startswith("t-")]
    new_id = f"t-{max(existing_ids or [0]) + 1:03d}"

    now = datetime.now().isoformat()
    task = {
        "id": new_id,
        "project_id": task_data.get("project_id", ""),
        "title": task_data["title"],
        "description": task_data.get("description", ""),
        "estimated_minutes": task_data.get("estimated_minutes", data["config"]["default_duration_minutes"]),
        "priority": task_data.get("priority", 3),
        "deadline": task_data.get("deadline"),
        "status": "pending",
        "scheduled_date": task_data.get("scheduled_date", date.today().isoformat()),
        "scheduled_time": task_data.get("scheduled_time", "09:00"),
        "tags": task_data.get("tags", []),
        "source": task_data.get("source", "manual"),
        "source_task_id": task_data.get("source_task_id"),
        "source_raw": task_data.get("source_raw"),
        "created_at": now,
        "updated_at": now
    }

    data["tasks"].append(task)
    save_json(TASKS_FILE, data)
    return task


def update_task(task_id, updates):
    """更新任务"""
    data = load_tasks()
    for t in data["tasks"]:
        if t["id"] == task_id:
            t.update(updates)
            t["updated_at"] = datetime.now().isoformat()
            save_json(TASKS_FILE, data)
            return t
    return None


def resolve_conflicts(choices):
    """
    处理冲突决策。
    choices: {"t-001": "keep", "t-002": "defer"}
    """
    tasks_data = load_tasks()
    tasks = tasks_data.get("tasks", [])
    config = tasks_data.get("config", {})

    # 应用选择
    for t in tasks:
        tid = t["id"]
        if tid in choices:
            if choices[tid] == "defer":
                # 顺延到下一个工作日
                sd = t.get("scheduled_date", date.today().isoformat())
                d = date.fromisoformat(sd) + timedelta(days=1)
                wd = set(config.get("working_days", [1, 2, 3, 4, 5]))
                while d.weekday() + 1 not in wd:
                    d += timedelta(days=1)
                t["scheduled_date"] = d.isoformat()
                t["scheduled_time"] = config["working_hours_start"]
                t["_deferred"] = True

    # 重新调度所有受影响的任务
    pending = [t for t in tasks if t.get("status") == "pending"]
    result = schedule_tasks(pending, config)
    schedule = result["schedule"]

    # 回写
    for day_key, day_data in schedule.items():
        for st in day_data.get("tasks", []):
            for t in tasks:
                if t["id"] == st["id"]:
                    t["scheduled_date"] = st.get("scheduled_date", t["scheduled_date"])
                    t["scheduled_time"] = st.get("scheduled_time", t["scheduled_time"])
                    t["updated_at"] = datetime.now().isoformat()

    save_json(TASKS_FILE, tasks_data)

    # 保存调度结果
    save_json(PLAN_FILE, result)

    return result


# ========== 工具函数 ==========

def _next_working_day(d, working_days):
    """下一个工作日"""
    d += timedelta(days=1)
    while d.weekday() + 1 not in working_days:
        d += timedelta(days=1)
    return d


# ========== CLI 入口 ==========

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("用法: python scheduler.py <command> [args]")
        print("  commands: plan [date], week [monday], add <json>, conflicts, resolve <json>")
        sys.exit(1)

    cmd = sys.argv[1]

    if cmd == "plan":
        d = sys.argv[2] if len(sys.argv) > 2 else None
        plan = generate_daily_plan(d)
        print(json.dumps(plan, ensure_ascii=False, indent=2))

    elif cmd == "week":
        m = sys.argv[2] if len(sys.argv) > 2 else None
        overview = generate_weekly_overview(m)
        print(json.dumps(overview, ensure_ascii=False, indent=2))

    elif cmd == "add":
        task_json = sys.argv[2] if len(sys.argv) > 2 else "{}"
        task_data = json.loads(task_json)
        result = add_task(task_data)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif cmd == "conflicts":
        tasks, config = aggregate_tasks()
        result = detect_conflicts(tasks, config)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    elif cmd == "resolve":
        choices_json = sys.argv[2] if len(sys.argv) > 2 else "{}"
        choices = json.loads(choices_json)
        result = resolve_conflicts(choices)
        print(json.dumps(result, ensure_ascii=False, indent=2))

    else:
        print(f"未知命令: {cmd}")
