import crypto from "node:crypto";
import { marked } from "marked";
import {
  StoreError,
  addCategory,
  createContentSnapshot,
  deleteArticle,
  discardDraft,
  generateSlug,
  getCategoryNames,
  getCategoryStats,
  getPublishStatus,
  listWorkingArticles,
  publishArticle,
  readWorkingArticle,
  removeUpload,
  removeCategory,
  renameCategory,
  safeSlug,
  saveUpload,
  unpublishArticle,
  writeArticle,
} from "./article-store";
import { convertWordToMarkdown, decodeWordDataUrl } from "./word-import";
import { currentSessionVersion, savePassword, verifyPassword } from "./cms-auth";
import {
  assignConsultation,
  createConsultation,
  createConsultationTeacher,
  deleteConsultation,
  deleteConsultationTeacher,
  getTeacherSessionVersion,
  listConsultations,
  listConsultationTeachers,
  updateConsultation,
  updateConsultationTeacher,
  verifyTeacherCredentials,
} from "./consultation-store";
import { buildSiteAtomic, tryAcquireSiteBuildLock, withSiteBuildLock } from "../../scripts/site-build.mjs";

const SECRET = process.env.CMS_SECRET || crypto.randomBytes(32).toString("hex");
const API_KEY = process.env.CMS_API_KEY || "";
const CAPTCHA_TTL = 5 * 60 * 1000;
const CAPTCHA_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
const MAX_LOGIN_FAILURES = 5;
const LOGIN_FAILURE_WINDOW = 10 * 60 * 1000;
const LOGIN_BLOCK_DURATION = 10 * 60 * 1000;
const loginAttempts = new Map<string, { count: number; first: number; blockedUntil: number }>();
type AuthContext =
  | { role: "admin"; id: "admin"; name: "管理员" }
  | { role: "customer_service" | "operator"; id: string; name: string };
type BuildState = {
  status: "idle" | "building" | "success" | "error";
  ok: boolean | null;
  log: string;
  startedAt: string | null;
  finishedAt: string | null;
};
let buildState: BuildState = { status: "idle", ok: null, log: "", startedAt: null, finishedAt: null };

const json = (data: unknown, status = 200, headers?: HeadersInit) => Response.json(data, { status, headers });

const safeEqual = (leftValue: unknown, rightValue: unknown): boolean => {
  const left = Buffer.from(String(leftValue));
  const right = Buffer.from(String(rightValue));
  return left.length === right.length && crypto.timingSafeEqual(left, right);
};

const sessionToken = async () => crypto.createHmac("sha256", SECRET)
  .update(`qiyouxue-cms-v2:${await currentSessionVersion()}`)
  .digest("hex");

function cookies(request: Request): Record<string, string> {
  return Object.fromEntries((request.headers.get("cookie") || "").split(";").filter(Boolean).map((part) => {
    const index = part.indexOf("=");
    return [part.slice(0, index).trim(), decodeURIComponent(part.slice(index + 1).trim())];
  }));
}

function apiKeyValid(request: Request): boolean {
  if (!API_KEY) return false;
  const auth = request.headers.get("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
  const key = auth || request.headers.get("x-api-key");
  return key ? safeEqual(key, API_KEY) : false;
}

const sessionAuthed = async (request: Request): Promise<boolean> => {
  const token = cookies(request).cms_session;
  return Boolean(token && safeEqual(token, await sessionToken()));
};

function teacherSessionMac(id: string, version: number): string {
  return crypto.createHmac("sha256", SECRET).update(`qiyouxue-teacher-v1:${id}:${version}`).digest("hex");
}

async function teacherSession(request: Request): Promise<AuthContext | null> {
  const token = cookies(request).cms_teacher_session || "";
  const [id, versionValue, mac] = token.split(".");
  const version = Number(versionValue);
  if (!id || !Number.isInteger(version) || !mac || !safeEqual(mac, teacherSessionMac(id, version))) return null;
  const currentVersion = await getTeacherSessionVersion(id);
  if (currentVersion !== version) return null;
  const teacher = (await listConsultationTeachers()).find((item) => item.id === id && item.enabled);
  return teacher ? { role: teacher.role, id: teacher.id, name: teacher.name } : null;
}

async function authContext(request: Request): Promise<AuthContext | null> {
  if (await sessionAuthed(request) || apiKeyValid(request)) return { role: "admin", id: "admin", name: "管理员" };
  return teacherSession(request);
}

async function bodyOf(request: Request): Promise<Record<string, unknown>> {
  try { return await request.json(); } catch { return {}; }
}

function cookieHeader(request: Request, name: string, value: string, maxAge: number): string {
  const forwarded = request.headers.get("x-forwarded-proto");
  const secure = new URL(request.url).protocol === "https:" || forwarded === "https";
  return `${name}=${value}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
}

function loginBlockedFor(ip: string): number {
  const state = loginAttempts.get(ip);
  return state?.blockedUntil && state.blockedUntil > Date.now()
    ? Math.ceil((state.blockedUntil - Date.now()) / 1000)
    : 0;
}

function loginFailed(ip: string): void {
  const now = Date.now();
  let state = loginAttempts.get(ip);
  if (!state || now - state.first > LOGIN_FAILURE_WINDOW) {
    state = { count: 0, first: now, blockedUntil: 0 };
  }
  state.count += 1;
  if (state.count >= MAX_LOGIN_FAILURES) state.blockedUntil = now + LOGIN_BLOCK_DURATION;
  loginAttempts.set(ip, state);
}

function randomCaptcha(length = 4): string {
  let code = "";
  for (let index = 0; index < length; index += 1) {
    code += CAPTCHA_CHARS[crypto.randomInt(CAPTCHA_CHARS.length)];
  }
  return code;
}

function captchaMac(code: unknown, expiresAt: number): string {
  return crypto.createHmac("sha256", SECRET)
    .update(`${String(code).toUpperCase()}|${expiresAt}`)
    .digest("hex");
}

function captchaValid(request: Request, answer: unknown, token?: unknown): boolean {
  const value = String(token || cookies(request).cms_captcha || "");
  if (!value || !answer) return false;
  const separator = value.lastIndexOf(".");
  const expiresAt = Number(value.slice(0, separator));
  const mac = value.slice(separator + 1);
  return Boolean(expiresAt && Date.now() <= expiresAt && safeEqual(mac, captchaMac(answer, expiresAt)));
}

function captchaSvg(code: string): string {
  const width = 132;
  const height = 46;
  const colors = ["#8E1C17", "#6F1511", "#D1251A", "#5c3a2e"];
  const randomBetween = (min: number, max: number) => crypto.randomInt(min, max + 1);
  const pick = <T>(items: T[]): T => items[crypto.randomInt(items.length)];
  let content = `<rect width="${width}" height="${height}" fill="#EAF3FF"/>`;
  for (let index = 0; index < 4; index += 1) {
    content += `<line x1="${randomBetween(0, width)}" y1="${randomBetween(0, height)}" x2="${randomBetween(0, width)}" y2="${randomBetween(0, height)}" stroke="${pick(colors)}" stroke-width="1" opacity="0.45"/>`;
  }
  [...code].forEach((character, index) => {
    const x = 16 + index * 28 + randomBetween(-3, 3);
    const y = 31 + randomBetween(-4, 4);
    const rotation = randomBetween(-26, 26);
    content += `<text x="${x}" y="${y}" font-family="Georgia,serif" font-weight="700" font-size="${randomBetween(24, 30)}" fill="${pick(colors)}" transform="rotate(${rotation} ${x} ${y})">${character}</text>`;
  });
  for (let index = 0; index < 26; index += 1) {
    content += `<circle cx="${randomBetween(0, width)}" cy="${randomBetween(0, height)}" r="1" fill="${pick(colors)}" opacity="0.5"/>`;
  }
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${content}</svg>`;
}

function requireSlug(value: string | undefined): string {
  const slug = safeSlug(value);
  if (!slug) throw new StoreError("网址标识不合法");
  return slug;
}

async function startContentBuild(change: () => Promise<void>): Promise<boolean> {
  if (buildState.status === "building") return false;
  const releaseLock = await tryAcquireSiteBuildLock();
  if (!releaseLock) return false;
  buildState = { status: "building", ok: null, log: "", startedAt: new Date().toISOString(), finishedAt: null };
  void (async () => {
    const snapshot = await createContentSnapshot();
    try {
      await change();
      const log = await buildSiteAtomic();
      buildState = { status: "success", ok: true, log, startedAt: buildState.startedAt, finishedAt: new Date().toISOString() };
    } catch (error) {
      let rollbackMessage = "";
      try { await snapshot.restore(); } catch (rollbackError) {
        rollbackMessage = `\n内容回滚失败：${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`;
      }
      const message = error instanceof Error ? error.message : String(error);
      const log = typeof error === "object" && error && "log" in error ? String(error.log || "") : "";
      buildState = {
        status: "error",
        ok: false,
        log: `${log}${log ? "\n" : ""}${message}${rollbackMessage}`,
        startedAt: buildState.startedAt,
        finishedAt: new Date().toISOString(),
      };
    }
  })().finally(releaseLock).catch((error) => {
    buildState = {
      status: "error",
      ok: false,
      log: error instanceof Error ? error.message : String(error),
      startedAt: buildState.startedAt,
      finishedAt: new Date().toISOString(),
    };
  });
  return true;
}

async function startOrConflict(change: () => Promise<void>): Promise<Response> {
  if (!await startContentBuild(change)) {
    return json({ error: "网站正在部署或生成静态页面，本次操作未受理，请稍后重试" }, 409, { "Retry-After": "5" });
  }
  return json({ ok: true, status: "building" });
}

export async function handleCmsApi(request: Request, routeValue: string, clientAddress = "unknown"): Promise<Response> {
  const route = routeValue.replace(/^\/+|\/+$/g, "");
  const parts = route.split("/").filter(Boolean);
  const method = request.method.toUpperCase();

  try {
    if (route === "captcha" && method === "GET") {
      const code = randomCaptcha();
      const expiresAt = Date.now() + CAPTCHA_TTL;
      const token = `${expiresAt}.${captchaMac(code, expiresAt)}`;
      return new Response(captchaSvg(code), {
        headers: {
          "Cache-Control": "no-store",
          "Content-Type": "image/svg+xml; charset=utf-8",
          "Set-Cookie": cookieHeader(request, "cms_captcha", token, CAPTCHA_TTL / 1000),
          "X-Captcha-Token": token,
        },
      });
    }

    if (route === "consultations/claim" && method === "POST") {
      const body = await bodyOf(request);
      const result = await assignConsultation({ ...body, page: body.page || request.headers.get("referer") || "" });
      return json({ ok: true, teacher: result.teacher }, 201);
    }

    if (route === "session") {
      if (method === "GET") {
        const context = await authContext(request);
        return json({ authed: Boolean(context), user: context });
      }
      if (method === "POST") {
        const blockedFor = loginBlockedFor(clientAddress);
        if (blockedFor) return json({ error: `尝试过于频繁，请 ${Math.ceil(blockedFor / 60)} 分钟后再试` }, 429);
        const body = await bodyOf(request);
        const clearCaptcha = cookieHeader(request, "cms_captcha", "", 0);
        if (!captchaValid(request, body.captcha, body.captchaToken)) {
          return json({ error: "验证码错误或已过期" }, 400, { "Set-Cookie": clearCaptcha });
        }
        const username = String(body.username || "").trim().toLowerCase();
        const teacher = username && username !== "admin" ? await verifyTeacherCredentials(username, body.password) : null;
        const adminValid = !teacher && (!username || username === "admin") && await verifyPassword(body.password);
        if (!teacher && !adminValid) {
          loginFailed(clientAddress);
          return json({ error: "登录账号或密码错误" }, 401, { "Set-Cookie": clearCaptcha });
        }
        loginAttempts.delete(clientAddress);
        const headers = new Headers();
        headers.append("Set-Cookie", clearCaptcha);
        if (teacher) {
          headers.append("Set-Cookie", cookieHeader(request, "cms_session", "", 0));
          const token = `${teacher.id}.${teacher.sessionVersion}.${teacherSessionMac(teacher.id, teacher.sessionVersion)}`;
          headers.append("Set-Cookie", cookieHeader(request, "cms_teacher_session", token, 86400));
          return json({ ok: true, user: { role: teacher.role, id: teacher.id, name: teacher.name } }, 200, headers);
        }
        headers.append("Set-Cookie", cookieHeader(request, "cms_teacher_session", "", 0));
        headers.append("Set-Cookie", cookieHeader(request, "cms_session", await sessionToken(), 86400));
        return json({ ok: true, user: { role: "admin", id: "admin", name: "管理员" } }, 200, headers);
      }
      if (method === "DELETE") {
        const headers = new Headers();
        headers.append("Set-Cookie", cookieHeader(request, "cms_session", "", 0));
        headers.append("Set-Cookie", cookieHeader(request, "cms_teacher_session", "", 0));
        return json({ ok: true }, 200, headers);
      }
    }

    if (route === "account/password" && method === "PUT") {
      if (!await sessionAuthed(request)) throw new StoreError("请先登录文章后台", 401);
      const body = await bodyOf(request);
      const currentPassword = String(body.currentPassword || "");
      const newPassword = String(body.newPassword || "");
      if (!await verifyPassword(currentPassword)) throw new StoreError("当前密码错误", 401);
      if (newPassword.length < 5) throw new StoreError("新密码至少需要 5 个字符");
      if (newPassword.length > 128) throw new StoreError("新密码不能超过 128 个字符");
      if (safeEqual(currentPassword, newPassword)) throw new StoreError("新密码不能与当前密码相同");
      await savePassword(newPassword);
      return json({ ok: true, requiresLogin: true }, 200, {
        "Set-Cookie": cookieHeader(request, "cms_session", "", 0),
      });
    }

    const context = await authContext(request);
    if (!context) throw new StoreError("请先登录后台", 401);

    if (route === "consultation-teachers") {
      if (context.role !== "admin") throw new StoreError("无权管理后台账号", 403);
      if (method === "GET") return json({ teachers: await listConsultationTeachers() });
      if (method === "POST") return json({ teacher: await createConsultationTeacher(await bodyOf(request)) }, 201);
    }

    if (parts[0] === "consultation-teachers" && parts[1] && parts.length === 2) {
      if (context.role !== "admin") throw new StoreError("无权管理后台账号", 403);
      if (method === "PUT") return json({ teacher: await updateConsultationTeacher(parts[1], await bodyOf(request)) });
      if (method === "DELETE") {
        await deleteConsultationTeacher(parts[1]);
        return json({ ok: true });
      }
    }

    if (route === "consultations") {
      if (context.role === "operator") throw new StoreError("运营账号无权访问用户咨询", 403);
      if (method === "GET") return json({ consultations: await listConsultations(context.role === "customer_service" ? context.id : undefined) });
      if (method === "POST") {
        if (context.role !== "admin") throw new StoreError("无权录入用户", 403);
        return json({ consultation: await createConsultation(await bodyOf(request)) }, 201);
      }
    }

    if (parts[0] === "consultations" && parts[1] && parts.length === 2) {
      if (context.role === "operator") throw new StoreError("运营账号无权处理用户咨询", 403);
      if (method === "PATCH") return json({ consultation: await updateConsultation(parts[1], await bodyOf(request), context.role === "customer_service" ? context.id : undefined) });
      if (method === "DELETE") {
        if (context.role !== "admin") throw new StoreError("无权删除用户", 403);
        await deleteConsultation(parts[1]);
        return json({ ok: true });
      }
    }

    if (context.role !== "admin" && context.role !== "operator") throw new StoreError("客服账号无权访问内容管理", 403);

    if (route === "categories") {
      if (method === "GET") return json({ categories: await getCategoryNames(), stats: await getCategoryStats() });
      const body = await bodyOf(request);
      if (method === "POST") {
        return startOrConflict(async () => { await addCategory(body.name); });
      }
      if (method === "PATCH") {
        return startOrConflict(async () => { await renameCategory(body.current, body.name); });
      }
      if (method === "DELETE") {
        return startOrConflict(async () => { await removeCategory(body.name, body.replacement); });
      }
    }

    if (route === "build" && method === "GET") return json(buildState);

    if (route === "articles") {
      if (method === "GET") return json(await listWorkingArticles());
      if (method === "POST") {
        const body = await bodyOf(request);
        if (!String(body.title || "").trim()) throw new StoreError("请填写文章标题");
        if (!String(body.body || "").trim()) throw new StoreError("请填写文章正文");
        const slug = await generateSlug(body.category);
        const article = await withSiteBuildLock(() => writeArticle(slug, body, true));
        return json({ ok: true, slug, publishStatus: await getPublishStatus(article) }, 201);
      }
    }

    if (route === "imports/word" && method === "POST") {
      const body = await bodyOf(request);
      const fileName = String(body.fileName || "").trim();
      const buffer = decodeWordDataUrl(body.dataUrl, fileName);
      const uploadedUrls: string[] = [];
      let createdSlug: string | null = null;
      try {
        const converted = await convertWordToMarkdown(buffer, fileName, async (dataUrl) => {
          const url = await saveUpload(dataUrl);
          uploadedUrls.push(url);
          return url;
        });
        const title = String(body.title || "").trim() || converted.suggestedTitle;
        const imported = await withSiteBuildLock(async () => {
          const slug = await generateSlug(body.category);
          createdSlug = slug;
          const article = await writeArticle(slug, { title, category: body.category, body: converted.body }, true);
          return { slug, publishStatus: await getPublishStatus(article) };
        });
        return json({
          ok: true,
          slug: imported.slug,
          title,
          publishStatus: imported.publishStatus,
          imageCount: converted.imageCount,
          warnings: converted.warnings,
        }, 201);
      } catch (error) {
        if (createdSlug) {
          await withSiteBuildLock(() => discardDraft(createdSlug as string)).catch(() => {});
        }
        await Promise.allSettled(uploadedUrls.map((url) => removeUpload(url)));
        throw error;
      }
    }

    if (parts[0] === "articles" && parts[1]) {
      const slug = requireSlug(parts[1]);
      if (parts.length === 3 && parts[2] === "draft" && method === "DELETE") {
        return json({ ok: true, ...await withSiteBuildLock(() => discardDraft(slug)) });
      }
      if (parts.length === 3 && parts[2] === "publish" && method === "POST") {
        return startOrConflict(async () => { await publishArticle(slug); });
      }
      if (parts.length === 3 && parts[2] === "unpublish" && method === "POST") {
        return startOrConflict(async () => { await unpublishArticle(slug); });
      }
      if (parts.length === 2 && method === "GET") {
        const article = await readWorkingArticle(slug);
        return json({ ...article, publishStatus: await getPublishStatus(article) });
      }
      if (parts.length === 2 && method === "PUT") {
        const body = await bodyOf(request);
        if (!String(body.title || "").trim()) throw new StoreError("请填写文章标题");
        if (!String(body.body || "").trim()) throw new StoreError("请填写文章正文");
        const article = await withSiteBuildLock(() => writeArticle(slug, body));
        return json({ ok: true, slug, publishStatus: await getPublishStatus(article) });
      }
      if (parts.length === 2 && method === "DELETE") {
        return startOrConflict(async () => { await deleteArticle(slug); });
      }
    }

    if (route === "preview" && method === "POST") {
      const body = await bodyOf(request);
      return json({ html: await marked.parse(String(body.body || "")) });
    }

    if (route === "uploads" && method === "POST") {
      const body = await bodyOf(request);
      return json({ ok: true, url: await saveUpload(body.dataUrl) });
    }

    return json({ error: "接口不存在" }, 404);
  } catch (error) {
    if (error instanceof StoreError) return json({ error: error.message, ...error.details }, error.status);
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return json({ error: "文章不存在" }, 404);
    console.error(error);
    return json({ error: error instanceof Error ? error.message : "服务器处理失败" }, 500);
  }
}
