#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# schedules_web.py —— 定时任务看板：标准库 HTTP 服务 + 静态页面

"""
只读看板，不提供任何写操作：
  GET /                    schedule.html
  GET /api/schedules       任务列表 + 统计
  GET /api/history?limit=N 执行记录（默认 100，最多保留条数）
  GET /health              存活检查

用 http.server.ThreadingHTTPServer 起在独立线程里，不引入 Flask 之类的依赖。
看板只是展示层，真正的调度在 scheduler.py，任务管理仍然走 schedule_* 工具。
"""

import json
import logging
import threading
from datetime import datetime
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

from scheduler import iso, parse_stored_dt, resolve_tz, schedule_enabled

logger = logging.getLogger("SchedulesWeb")

PAGE_DIR = Path(__file__).resolve().parent
PAGE_FILE = PAGE_DIR / "schedule.html"
DEFAULT_HISTORY_LIMIT = 100


def describe_schedule(task):
    """把任务规格压成一句人话，给页面表格用。"""
    kind = task.get("kind")
    spec = task.get("spec") or {}
    if kind == "once":
        return f"一次性 · {spec.get('at', '')}"
    if kind == "interval":
        try:
            minutes = max(1, int(spec.get("every_seconds") or 60) // 60)
        except (TypeError, ValueError):
            minutes = 1
        return f"每 {minutes} 分钟"
    if kind == "daily":
        return f"每天 {spec.get('time', '')}"
    if kind == "cron":
        return f"cron · {spec.get('cron', '')}"
    return str(kind or "未知")


def _today_runs(records, now):
    tz = resolve_tz()
    today = now.astimezone(tz).date()
    count = 0
    for rec in records:
        if rec.get("status") == "skipped":
            continue
        dt = parse_stored_dt(rec.get("ts"))
        if dt is not None and dt.astimezone(tz).date() == today:
            count += 1
    return count


def build_schedules_payload(store, history):
    now = datetime.now(resolve_tz())
    tasks = []
    for task in store.list_tasks():
        item = dict(task)
        item["schedule"] = describe_schedule(task)
        tasks.append(item)

    records = history.list(limit=history.max_records) if history else []
    counts = {
        "total": len(tasks),
        "enabled": sum(1 for t in tasks if t.get("enabled")),
        "today_runs": _today_runs(records, now),
        "last_run": records[0].get("ts") if records else None,
    }
    return {
        "ok": True,
        "now": iso(now),
        "timezone": str(resolve_tz()),
        "scheduler_enabled": schedule_enabled(),
        "counts": counts,
        "tasks": tasks,
    }


def build_history_payload(history, limit=DEFAULT_HISTORY_LIMIT):
    if history is None:
        return {"ok": True, "timezone": str(resolve_tz()), "records": []}
    return {
        "ok": True,
        "timezone": str(resolve_tz()),
        "records": history.list(limit=limit),
    }


def _parse_limit(query, default=DEFAULT_HISTORY_LIMIT, maximum=500):
    raw = (query.get("limit") or [None])[0]
    if raw is None:
        return default
    try:
        value = int(raw)
    except (TypeError, ValueError):
        return default
    return max(1, min(value, maximum))


def make_handler(store, history):
    class SchedulesHandler(BaseHTTPRequestHandler):
        server_version = "SchedulesWeb/1.0"
        protocol_version = "HTTP/1.1"

        def log_message(self, fmt, *args):
            logger.debug("%s - %s", self.address_string(), fmt % args)

        # ---------- 响应封装 ----------
        def _send(self, status, body, content_type):
            data = body if isinstance(body, bytes) else body.encode("utf-8")
            self.send_response(status)
            self.send_header("Content-Type", content_type)
            if status != 204:
                self.send_header("Content-Length", str(len(data)))
            self.send_header("Cache-Control", "no-store")
            self.send_header("X-Content-Type-Options", "nosniff")
            self.end_headers()
            if self.command != "HEAD":
                self.wfile.write(data)

        def _json(self, status, payload):
            self._send(status, json.dumps(payload, ensure_ascii=False),
                       "application/json; charset=utf-8")

        def _page(self):
            try:
                html = PAGE_FILE.read_bytes()
            except OSError as e:
                logger.error(f"读取看板页面失败（{PAGE_FILE}）：{e}")
                return self._json(500, {"ok": False, "error": "看板页面缺失"})
            self._send(200, html, "text/html; charset=utf-8")

        # ---------- 路由 ----------
        def do_HEAD(self):
            self.do_GET()

        def do_GET(self):
            parsed = urlparse(self.path)
            path = parsed.path or "/"
            query = parse_qs(parsed.query)

            try:
                if path in ("/", "/index.html", "/schedule.html", "/schedules.html"):
                    return self._page()
                if path == "/api/schedules":
                    return self._json(200, build_schedules_payload(store, history))
                if path == "/api/history":
                    limit = _parse_limit(query)
                    return self._json(200, build_history_payload(history, limit))
                if path == "/health":
                    return self._json(200, {"ok": True})
                if path == "/favicon.ico":
                    return self._send(204, b"", "image/x-icon")
                self._json(404, {"ok": False, "error": "not found"})
            except Exception as e:
                logger.error(f"看板接口异常（{path}）：{e}", exc_info=True)
                try:
                    self._json(500, {"ok": False, "error": str(e)})
                except Exception:
                    pass

    return SchedulesHandler


def create_server(host, port, store, history):
    httpd = ThreadingHTTPServer((host, int(port)), make_handler(store, history))
    httpd.daemon_threads = True
    return httpd


def start_in_thread(host, port, store, history):
    """起在后台线程，返回 httpd；调用方负责 shutdown()。"""
    httpd = create_server(host, port, store, history)
    thread = threading.Thread(target=httpd.serve_forever,
                              kwargs={"poll_interval": 0.5},
                              name="schedules-web", daemon=True)
    thread.start()
    return httpd
