import { Hono } from "hono";
import {
  availabilityAndTimeline,
  availabilityAndTimelineBatch,
  getAppSettings,
  getMonitor,
  listMonitorsPublic,
  normMonitorId,
  updateAppSettings,
} from "./db";
import {
  createMonitorFromPayload,
  deleteMonitor,
  runScheduled,
  runSingleProbe,
  updateMonitorFromPayload,
} from "./scheduler";

type MonitorCreateBody = {
  display_name: string;
  api_base_url: string;
  api_key: string;
  model: string;
  protocol: "openai" | "openai_responses" | "claude";
  category?: string;
  enabled?: boolean;
  /** 未传时默认为流式 */
  probe_stream?: boolean;
  /** 未传时默认在首页展示 */
  show_on_dashboard?: boolean;
};

function parseOptionalBool01(o: Record<string, unknown>, key: string): boolean | undefined {
  const v = o[key];
  if (typeof v === "boolean") return v;
  if (v === 0) return false;
  if (v === 1) return true;
  return undefined;
}

function parseOptionalProbeStream(o: Record<string, unknown>): boolean | undefined {
  return parseOptionalBool01(o, "probe_stream");
}

const app = new Hono<{ Bindings: Env }>();

/** 避免边缘/浏览器把动态 JSON 缓存成「半套字段」，尤其带 Authorization 的请求易被错误复用 */
app.use("/api/*", async (c, next) => {
  c.header("Cache-Control", "private, no-store, max-age=0");
  await next();
});

const ALLOWED = new Set([1, 5, 10, 30, 60, 360]);

function checkAdmin(c: { req: { header: (k: string) => string | undefined }; env: Env }): Response | null {
  const configured = (c.env.ADMIN_TOKEN ?? "").trim();
  if (!configured) {
    return new Response(
      JSON.stringify({
        error: "admin_token_not_configured",
        message: "Configure ADMIN_TOKEN in .dev.vars (local) or wrangler secret put ADMIN_TOKEN (production).",
      }),
      { status: 503, headers: { "Content-Type": "application/json" } }
    );
  }
  const h = c.req.header("Authorization") ?? "";
  const m = /^Bearer\s+(.+)$/i.exec(h);
  const bearer = (m?.[1] ?? "").trim();
  if (bearer !== configured) {
    return new Response(JSON.stringify({ error: "unauthorized" }), {
      status: 401,
      headers: { "Content-Type": "application/json" },
    });
  }
  return null;
}

async function parseMonitorCreate(c: { req: { json: () => Promise<unknown> } }): Promise<MonitorCreateBody | null> {
  let body: unknown;
  try {
    body = await c.req.json();
  } catch {
    return null;
  }
  if (!body || typeof body !== "object") return null;
  const o = body as Record<string, unknown>;
  const display_name = typeof o.display_name === "string" ? o.display_name : "";
  const api_base_url = typeof o.api_base_url === "string" ? o.api_base_url : "";
  const api_key = typeof o.api_key === "string" ? o.api_key : "";
  const model = typeof o.model === "string" ? o.model : "";
  const protocol =
    o.protocol === "openai" || o.protocol === "openai_responses" || o.protocol === "claude" ? o.protocol : null;
  if (!display_name.trim() || !api_base_url.trim() || !api_key.trim() || !model.trim() || !protocol) {
    return null;
  }
  const category = typeof o.category === "string" ? o.category : undefined;
  const enabled = typeof o.enabled === "boolean" ? o.enabled : undefined;
  const probe_stream = parseOptionalProbeStream(o);
  const show_on_dashboard = parseOptionalBool01(o, "show_on_dashboard");
  return {
    display_name,
    api_base_url,
    api_key,
    model,
    protocol,
    category,
    enabled,
    probe_stream,
    show_on_dashboard,
  };
}

type MonitorRowWithInterval = Awaited<ReturnType<typeof listMonitorsPublic>>[number] & {
  interval_minutes: number;
};

const TIMELINE_SHANGHAI_OFF = 8 * 3600;
const TIMELINE_HALF_DAY_SEC = 12 * 60 * 60;
const TIMELINE_SLOTS = 60;

function currentTimelineSlotStart(nowSec: number): number {
  return Math.floor((nowSec + TIMELINE_SHANGHAI_OFF) / TIMELINE_HALF_DAY_SEC) * TIMELINE_HALF_DAY_SEC - TIMELINE_SHANGHAI_OFF;
}

function timelineDailyFromBuckets(daily: Array<{ dayStart: number; ok: number; total: number }>) {
  const bySlot = new Map(daily.map((d) => [d.dayStart, d]));
  const current = currentTimelineSlotStart(Math.floor(Date.now() / 1000));
  const first = current - (TIMELINE_SLOTS - 1) * TIMELINE_HALF_DAY_SEC;

  return Array.from({ length: TIMELINE_SLOTS }, (_, i) => {
    const t = first + i * TIMELINE_HALF_DAY_SEC;
    const d = bySlot.get(t);
    const total = d?.total ?? 0;
    const ratio = total > 0 ? (d?.ok ?? 0) / total : 0;
    return {
      t,
      up: total > 0 && (d?.ok ?? 0) === total,
      ratio,
      hasData: total > 0,
    };
  });
}

async function monitorsDtoList(db: D1Database, merged: MonitorRowWithInterval[]) {
  const statsMap = await availabilityAndTimelineBatch(
    db,
    merged.map((m) => m.id)
  );
  return merged.map((m) => {
    const stats = statsMap.get(normMonitorId(m.id));
    if (!stats) {
      return {
        ...m,
        availability24h: null,
        availability30d: null,
        probe_count: 0,
        lastProbe: null,
        timelineDaily: [],
      };
    }
    const timelineDaily = timelineDailyFromBuckets(stats.daily);
    return {
      ...m,
      availability24h: stats.availability24h,
      availability30d: stats.availability30d,
      probe_count: stats.probe_count,
      lastProbe: stats.lastProbe,
      timelineDaily,
    };
  });
}

app.get("/api/monitors", async (c) => {
  const db = c.env.DB;
  const [rows, s] = await Promise.all([listMonitorsPublic(db, "dashboard"), getAppSettings(db)]);
  const merged: MonitorRowWithInterval[] = rows.map((m) => ({
    ...m,
    interval_minutes: s.probe_interval_minutes,
  }));
  return c.json(await monitorsDtoList(db, merged));
});

app.get("/api/monitors/:id", async (c) => {
  const id = c.req.param("id");
  const db = c.env.DB;
  const [m, s] = await Promise.all([getMonitor(db, id), getAppSettings(db)]);
  if (!m) return c.json({ error: "not_found" }, 404);
  if (m.show_on_dashboard === 0) return c.json({ error: "not_found" }, 404);
  const stats = await availabilityAndTimeline(db, id);
  const timelineDaily = timelineDailyFromBuckets(stats.daily);
  return c.json({
    id: m.id,
    display_name: m.display_name,
    api_base_url: m.api_base_url,
    model: m.model,
    protocol: m.protocol,
    interval_minutes: s.probe_interval_minutes,
    probe_stream: m.probe_stream,
    show_on_dashboard: m.show_on_dashboard,
    enabled: m.enabled,
    category: m.category,
    created_at: m.created_at,
    last_run_at: m.last_run_at,
    next_run_at: m.next_run_at,
    availability24h: stats.availability24h,
    availability30d: stats.availability30d,
    probe_count: stats.probe_count,
    lastProbe: stats.lastProbe,
    timelineDaily,
  });
});

app.get("/api/admin/ping", (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  return c.json({ ok: true });
});

app.get("/api/admin/probe-settings", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  const s = await getAppSettings(c.env.DB);
  return c.json({
    probe_prompts: s.probe_prompts,
    probe_interval_minutes: s.probe_interval_minutes,
  });
});

app.put("/api/admin/probe-settings", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  let body: Record<string, unknown>;
  try {
    body = (await c.req.json()) as Record<string, unknown>;
  } catch {
    return c.json({ error: "invalid_json" }, 400);
  }
  const patch: Partial<{ probe_prompts: string; probe_interval_minutes: number }> = {};
  if (typeof body.probe_prompts === "string") patch.probe_prompts = body.probe_prompts;
  if (typeof body.probe_interval_minutes === "number") {
    if (!ALLOWED.has(body.probe_interval_minutes)) {
      return c.json({ error: "invalid_interval" }, 400);
    }
    patch.probe_interval_minutes = body.probe_interval_minutes as 1 | 5 | 10 | 30 | 60 | 360;
  }
  if (Object.keys(patch).length === 0) return c.json({ error: "invalid_body" }, 400);
  try {
    await updateAppSettings(c.env.DB, patch);
    return c.json({ ok: true });
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    return c.json({ error: "update_failed", message }, 500);
  }
});

app.get("/api/admin/monitors", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  const db = c.env.DB;
  const [rows, s] = await Promise.all([listMonitorsPublic(db, "all"), getAppSettings(db)]);
  const merged: MonitorRowWithInterval[] = rows.map((m) => ({
    ...m,
    interval_minutes: s.probe_interval_minutes,
  }));
  return c.json(await monitorsDtoList(db, merged));
});

app.post("/api/admin/monitors", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  const parsed = await parseMonitorCreate(c);
  if (!parsed) return c.json({ error: "invalid_body" }, 400);
  const { id } = await createMonitorFromPayload(c.env, c.env.DB, parsed);
  return c.json({ id }, 201);
});

app.put("/api/admin/monitors/:id", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  const id = c.req.param("id");
  let body: Record<string, unknown>;
  try {
    body = (await c.req.json()) as Record<string, unknown>;
  } catch {
    return c.json({ error: "invalid_json" }, 400);
  }
  const patch: Parameters<typeof updateMonitorFromPayload>[3] = {};
  if (typeof body.display_name === "string") patch.display_name = body.display_name;
  if (typeof body.api_base_url === "string") patch.api_base_url = body.api_base_url;
  if (typeof body.api_key === "string") patch.api_key = body.api_key;
  if (typeof body.model === "string") patch.model = body.model;
  if (body.protocol === "openai" || body.protocol === "openai_responses" || body.protocol === "claude")
    patch.protocol = body.protocol;
  if (typeof body.category === "string") patch.category = body.category;
  if (typeof body.enabled === "boolean") patch.enabled = body.enabled;
  if (typeof body.probe_stream === "boolean") patch.probe_stream = body.probe_stream;
  else if (body.probe_stream === 0 || body.probe_stream === 1) patch.probe_stream = body.probe_stream === 1;
  if (typeof body.show_on_dashboard === "boolean") patch.show_on_dashboard = body.show_on_dashboard;
  else if (body.show_on_dashboard === 0 || body.show_on_dashboard === 1)
    patch.show_on_dashboard = body.show_on_dashboard === 1;
  try {
    const ok = await updateMonitorFromPayload(c.env, c.env.DB, id, patch);
    if (!ok) return c.json({ error: "not_found" }, 404);
    return c.json({ ok: true });
  } catch (e) {
    const message = e instanceof Error ? e.message : String(e);
    return c.json({ error: "update_failed", message }, 500);
  }
});

app.delete("/api/admin/monitors/:id", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  const id = c.req.param("id");
  const m = await getMonitor(c.env.DB, id);
  if (!m) return c.json({ error: "not_found" }, 404);
  await deleteMonitor(c.env.DB, id);
  return c.json({ ok: true });
});

app.post("/api/admin/monitors/:id/run", async (c) => {
  const err = checkAdmin(c);
  if (err) return err;
  const id = c.req.param("id");
  const m = await getMonitor(c.env.DB, id);
  if (!m) return c.json({ error: "not_found" }, 404);
  const nowSec = Math.floor(Date.now() / 1000);
  const outcome = await runSingleProbe(c.env, id, c.env.DB, nowSec);
  if (!outcome.success) {
    return c.json({ ok: false, error: outcome.error }, 200);
  }
  return c.json({ ok: true });
});

export default {
  fetch: app.fetch,
  scheduled: (_event: ScheduledEvent, env: Env, ctx: ExecutionContext) => {
    ctx.waitUntil(runScheduled(env));
  },
};
