import "server-only";

import { createHash } from "crypto";
import type { ResultSetHeader, RowDataPacket } from "mysql2/promise";
import sanitizeHtml from "sanitize-html";
import { getDbPool } from "@/lib/db";
import {
  getGoogleIntegrationAccess,
  GOOGLE_BLOGGER_SCOPE,
} from "@/lib/google-search-console";
import {
  ensureMarketingBlogSchema,
  getMarketingBlogPostById,
  type MarketingBlogPost,
} from "@/lib/marketing-blog";

declare global {
  var __bloggerSyndicationSchemaPromise: Promise<void> | undefined;
}

const BLOGGER_API_BASE_URL = "https://www.googleapis.com/blogger/v3";
const DEFAULT_BLOGGER_BLOG_ID = "1328335945575071022";
const DEFAULT_BLOGGER_BLOG_URL = "https://officialmail-blog.blogspot.com";
const DEFAULT_SOURCE_SITE_URL = "https://officialsite.kr";

type BloggerPostResponse = {
  error?: { message?: unknown };
  id?: unknown;
  status?: unknown;
  title?: unknown;
  updated?: unknown;
  url?: unknown;
};

type BloggerSyndicationRow = RowDataPacket & {
  external_post_id: string;
  external_url: string | null;
  last_error: string | null;
  platform_status: "draft" | "failed" | "published";
  post_id: number;
  source_fingerprint: string;
  synced_at: Date | null;
  updated_at: Date;
};

export type BloggerPostStatus = {
  editUrl: string;
  externalPostId: string;
  externalUrl: string | null;
  lastError: string | null;
  postId: number;
  sourceChanged: boolean;
  status: "draft" | "failed" | "published";
  syncedAt: string | null;
};

export type BloggerIntegrationStatus = {
  accountEmail: string | null;
  blogId: string | null;
  blogName: string;
  blogUrl: string | null;
  configured: boolean;
  connected: boolean;
  permissionGranted: boolean;
  post: BloggerPostStatus | null;
};

function getBloggerConfig() {
  const blogId = process.env.GOOGLE_BLOGGER_BLOG_ID?.trim() || DEFAULT_BLOGGER_BLOG_ID;
  const blogUrl = (
    process.env.GOOGLE_BLOGGER_BLOG_URL?.trim() || DEFAULT_BLOGGER_BLOG_URL
  ).replace(/\/$/, "");
  const sourceSiteUrl = (
    process.env.GOOGLE_BLOGGER_SOURCE_SITE_URL?.trim() || DEFAULT_SOURCE_SITE_URL
  ).replace(/\/$/, "");

  if (!blogId || !blogUrl) {
    return null;
  }

  return {
    blogId,
    blogName: "오피셜메일 블로그",
    blogUrl,
    sourceSiteUrl,
  };
}

async function ensureBloggerSyndicationSchema() {
  if (!global.__bloggerSyndicationSchemaPromise) {
    global.__bloggerSyndicationSchemaPromise = (async () => {
      await ensureMarketingBlogSchema();
      await getDbPool().query(`
        CREATE TABLE IF NOT EXISTS marketing_blog_syndications (
          id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
          post_id BIGINT UNSIGNED NOT NULL,
          platform VARCHAR(32) NOT NULL,
          external_post_id VARCHAR(128) NOT NULL,
          external_url VARCHAR(768) NULL,
          platform_status ENUM('draft', 'published', 'failed') NOT NULL DEFAULT 'draft',
          source_fingerprint CHAR(64) NOT NULL,
          last_error TEXT NULL,
          synced_at DATETIME NULL,
          created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
          updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
          PRIMARY KEY (id),
          UNIQUE KEY uq_marketing_blog_syndications_post_platform (post_id, platform),
          UNIQUE KEY uq_marketing_blog_syndications_external (platform, external_post_id),
          KEY idx_marketing_blog_syndications_status (platform, platform_status, updated_at),
          CONSTRAINT fk_marketing_blog_syndications_post
            FOREIGN KEY (post_id) REFERENCES marketing_blog_posts(id) ON DELETE CASCADE
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
      `);
    })().catch((error) => {
      global.__bloggerSyndicationSchemaPromise = undefined;
      throw error;
    });
  }

  await global.__bloggerSyndicationSchemaPromise;
}

function escapeHtml(value: string) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function absoluteUrl(value: string, sourceSiteUrl: string) {
  try {
    const url = new URL(value, `${sourceSiteUrl}/`);
    return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : "";
  } catch {
    return "";
  }
}

function renderInline(value: string, sourceSiteUrl: string) {
  let html = escapeHtml(value);

  html = html.replace(/\[([^\]]+)\]\(([^\s)]+)\)/g, (_match, label: string, href: string) => {
    const safeHref = absoluteUrl(href, sourceSiteUrl);
    return safeHref
      ? `<a href="${escapeHtml(safeHref)}" style="color:#2f6df6;text-decoration:underline;text-underline-offset:3px">${label}</a>`
      : label;
  });
  html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
  html = html.replace(/~~([^~]+)~~/g, "<s>$1</s>");
  html = html.replace(/==([^=]+)==/g, "<u>$1</u>");
  html = html.replace(/`([^`]+)`/g, '<code style="background:#f2f5fa;border-radius:5px;padding:2px 5px">$1</code>');

  return html;
}

function renderImage(line: string, sourceSiteUrl: string) {
  const match = line.match(/^!\[([^\]]*)\]\(([^\s)]+)\)(?:\{[^}]*\})?$/);

  if (!match) return null;

  const src = absoluteUrl(match[2], sourceSiteUrl);

  if (!src) return null;

  const caption = match[1].trim();
  return [
    '<figure style="margin:28px auto;text-align:center">',
    `<img src="${escapeHtml(src)}" alt="${escapeHtml(caption)}" style="display:block;width:100%;height:auto;border-radius:10px" />`,
    caption ? `<figcaption style="margin-top:8px;color:#8492a6;font-size:12px;line-height:1.6">${escapeHtml(caption)}</figcaption>` : "",
    "</figure>",
  ].join("");
}

function tableCells(value: string) {
  return value.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
}

function isTableDivider(value: string) {
  return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(value.trim());
}

function renderRichBlock(
  lines: string[],
  startIndex: number,
  sourceSiteUrl: string,
) {
  const header = lines[startIndex].trim();
  const match = header.match(/^:::official-block\{type=([a-z0-9-]+);title=([^;]*);url=([^}]*)\}$/);

  if (!match) return null;

  const endIndex = lines.findIndex(
    (line, index) => index > startIndex && line.trim() === ":::official-block-end",
  );

  if (endIndex < 0) return null;

  const kind = match[1];
  const title = decodeURIComponent(match[2] || "");
  const urlValue = decodeURIComponent(match[3] || "");
  const bodyLines = lines.slice(startIndex + 1, endIndex).filter((line) => line.trim());
  let html = "";

  if (kind === "callout") {
    html = `<aside style="margin:24px 0;padding:16px;border-radius:10px;background:#f5f7fa;color:#526784;line-height:1.8">${title ? `<strong style="margin-right:8px">${escapeHtml(title)}</strong>` : ""}${renderInline(bodyLines.join(" "), sourceSiteUrl)}</aside>`;
  } else if (kind === "button" || kind === "page") {
    const href = absoluteUrl(urlValue, sourceSiteUrl);
    html = href ? `<p style="margin:24px 0"><a href="${escapeHtml(href)}" style="display:inline-block;border-radius:9px;background:#2f6df6;color:#fff;padding:11px 17px;text-decoration:none;font-weight:700">${escapeHtml(title || "자세히 보기")}</a></p>` : "";
  } else if (kind === "video") {
    const href = absoluteUrl(urlValue, sourceSiteUrl);
    html = href ? `<p style="margin:24px 0"><a href="${escapeHtml(href)}">${escapeHtml(title || "동영상 보기")}</a></p>` : "";
  } else if (kind === "audio" || kind === "file") {
    const href = absoluteUrl(urlValue, sourceSiteUrl);
    html = href ? `<p style="margin:24px 0"><a href="${escapeHtml(href)}">${escapeHtml(title || "파일 열기")}</a></p>` : "";
  } else if (kind === "table" || kind === "database-table") {
    const rows = bodyLines.map(tableCells);
    const headers = rows.shift() ?? [];
    html = `<div style="margin:26px 0;overflow-x:auto">${title ? `<p style="font-weight:700">${escapeHtml(title)}</p>` : ""}<table style="width:100%;min-width:520px;border-collapse:collapse"><thead><tr>${headers.map((cell) => `<th style="border:1px solid #e3e9f3;background:#f8faff;padding:9px;text-align:left">${renderInline(cell, sourceSiteUrl)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${headers.map((_, index) => `<td style="border:1px solid #e3e9f3;padding:9px">${renderInline(row[index] || "", sourceSiteUrl)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`;
  } else if (kind.startsWith("columns-")) {
    const count = Math.max(2, Math.min(4, Number(kind.slice(-1)) || 2));
    html = `<table style="width:100%;margin:24px 0;border-collapse:separate;border-spacing:10px"><tr>${Array.from({ length: count }, (_, index) => `<td style="vertical-align:top;background:#f7f9fc;border-radius:8px;padding:12px">${renderInline(bodyLines[index] || "", sourceSiteUrl)}</td>`).join("")}</tr></table>`;
  } else if (kind.startsWith("toggle")) {
    html = `<details style="margin:22px 0"><summary style="font-weight:700">${escapeHtml(title)}</summary><p style="line-height:1.8">${renderInline(bodyLines.join(" "), sourceSiteUrl)}</p></details>`;
  } else if (bodyLines.length > 0) {
    html = `<section style="margin:22px 0">${title ? `<h3>${escapeHtml(title)}</h3>` : ""}<p style="line-height:1.8">${renderInline(bodyLines.join(" "), sourceSiteUrl)}</p></section>`;
  }

  return { html, nextIndex: endIndex + 1 };
}

function selectSyndicationLines(markdown: string) {
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
  const contentLength = lines.reduce((sum, line) => sum + line.trim().length, 0);
  const targetLength = Math.max(900, Math.round(contentLength * 0.52));
  const selected: string[] = [];
  let selectedLength = 0;
  let insideRichBlock = false;

  for (const line of lines) {
    const trimmed = line.trim();

    if (/^##\s+(자주 묻는 질문|FAQ)/i.test(trimmed) && selectedLength >= targetLength * 0.8) {
      break;
    }

    selected.push(line);
    selectedLength += trimmed.length;

    if (trimmed.startsWith(":::official-block{")) insideRichBlock = true;
    if (trimmed === ":::official-block-end") insideRichBlock = false;

    if (!insideRichBlock && selectedLength >= targetLength && trimmed === "") {
      break;
    }
  }

  return selected;
}

function markdownToBloggerHtml(markdown: string, sourceSiteUrl: string) {
  const lines = selectSyndicationLines(markdown);
  const output: string[] = [];
  let index = 0;

  while (index < lines.length) {
    const line = lines[index].trim();

    if (!line) {
      index += 1;
      continue;
    }

    const richBlock = renderRichBlock(lines, index, sourceSiteUrl);

    if (richBlock) {
      output.push(richBlock.html);
      index = richBlock.nextIndex;
      continue;
    }

    const image = renderImage(line, sourceSiteUrl);

    if (image) {
      output.push(image);
      index += 1;
      continue;
    }

    if (line.startsWith("|") && index + 1 < lines.length && isTableDivider(lines[index + 1])) {
      const headers = tableCells(line);
      const rows: string[][] = [];
      index += 2;

      while (index < lines.length && lines[index].trim().startsWith("|")) {
        rows.push(tableCells(lines[index]));
        index += 1;
      }

      output.push(`<div style="margin:26px 0;overflow-x:auto"><table style="width:100%;min-width:520px;border-collapse:collapse"><thead><tr>${headers.map((cell) => `<th style="border:1px solid #e3e9f3;background:#f8faff;padding:9px;text-align:left">${renderInline(cell, sourceSiteUrl)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${headers.map((_, cellIndex) => `<td style="border:1px solid #e3e9f3;padding:9px">${renderInline(row[cellIndex] || "", sourceSiteUrl)}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`);
      continue;
    }

    const heading = line.match(/^(#{1,4})\s+(.+)$/);

    if (heading) {
      const level = Math.min(4, Math.max(2, heading[1].length));
      output.push(`<h${level} style="margin:30px 0 12px;color:#16233c;line-height:1.35">${renderInline(heading[2], sourceSiteUrl)}</h${level}>`);
      index += 1;
      continue;
    }

    if (/^[-*]\s+/.test(line)) {
      const items: string[] = [];

      while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
        items.push(lines[index].trim().replace(/^[-*]\s+/, ""));
        index += 1;
      }

      output.push(`<ul style="margin:16px 0;padding-left:22px;line-height:1.85">${items.map((item) => `<li>${renderInline(item, sourceSiteUrl)}</li>`).join("")}</ul>`);
      continue;
    }

    if (/^\d+\.\s+/.test(line)) {
      const items: string[] = [];

      while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
        items.push(lines[index].trim().replace(/^\d+\.\s+/, ""));
        index += 1;
      }

      output.push(`<ol style="margin:16px 0;padding-left:22px;line-height:1.85">${items.map((item) => `<li>${renderInline(item, sourceSiteUrl)}</li>`).join("")}</ol>`);
      continue;
    }

    if (line.startsWith(">")) {
      output.push(`<blockquote style="margin:20px 0;border-left:3px solid #b9cdf8;padding:4px 0 4px 14px;color:#526784;line-height:1.8">${renderInline(line.replace(/^>\s?/, ""), sourceSiteUrl)}</blockquote>`);
      index += 1;
      continue;
    }

    if (/^---+$/.test(line)) {
      output.push('<hr style="margin:28px 0;border:0;border-top:1px solid #e5ebf4" />');
      index += 1;
      continue;
    }

    const paragraph = [line];
    index += 1;

    while (
      index < lines.length &&
      lines[index].trim() &&
      !/^#{1,4}\s+|^[-*]\s+|^\d+\.\s+|^>\s?|^\||^:::official-block\{|^!\[/.test(lines[index].trim())
    ) {
      paragraph.push(lines[index].trim());
      index += 1;
    }

    output.push(`<p style="margin:14px 0;color:#42536f;font-size:16px;line-height:1.9">${renderInline(paragraph.join(" "), sourceSiteUrl)}</p>`);
  }

  return sanitizeHtml(output.join("\n"), {
    allowedTags: sanitizeHtml.defaults.allowedTags.concat(["figure", "figcaption", "img", "details", "summary"]),
    allowedAttributes: {
      ...sanitizeHtml.defaults.allowedAttributes,
      "*": ["style"],
      a: ["href", "style", "target", "rel"],
      img: ["src", "alt", "style"],
    },
    allowedSchemes: ["http", "https", "mailto"],
  });
}

export function renderExternalBlogHtml(markdown: string, sourceSiteUrl: string) {
  return markdownToBloggerHtml(markdown, sourceSiteUrl);
}

function bloggerPayload(post: MarketingBlogPost, sourceSiteUrl: string) {
  const sourceUrl = `${sourceSiteUrl}/blog/${post.slug}`;
  const content = markdownToBloggerHtml(post.contentMarkdown, sourceSiteUrl);
  const sourceLink = [
    '<section style="margin-top:34px;border-top:1px solid #e5ebf4;padding-top:22px">',
    '<p style="margin:0 0 12px;color:#7183a1;font-size:13px">오피셜메일 공식 가이드의 핵심 내용을 재구성한 글입니다.</p>',
    `<a href="${escapeHtml(sourceUrl)}" style="display:inline-block;border-radius:9px;background:#2f6df6;color:#fff;padding:12px 18px;text-decoration:none;font-weight:700">원문에서 계속 읽기</a>`,
    "</section>",
  ].join("");

  return {
    content: `${content}${sourceLink}`,
    labels: Array.from(new Set(["오피셜메일", ...post.keywords])).slice(0, 12),
    title: `${post.title} 핵심 정리`,
  };
}

function sourceFingerprint(post: MarketingBlogPost) {
  return createHash("sha256")
    .update([post.title, post.contentMarkdown, post.updatedAt].join("\n"))
    .digest("hex");
}

async function bloggerRequest<T>(
  accessToken: string,
  path: string,
  init?: RequestInit,
) {
  const response = await fetch(`${BLOGGER_API_BASE_URL}${path}`, {
    ...init,
    headers: {
      authorization: `Bearer ${accessToken}`,
      ...(init?.body ? { "content-type": "application/json" } : {}),
      ...init?.headers,
    },
    cache: "no-store",
  });
  const payload = (await response.json().catch(() => ({}))) as T & BloggerPostResponse;

  if (!response.ok) {
    throw new Error(String(payload.error?.message || "Blogger API 요청에 실패했습니다."));
  }

  return payload;
}

async function getSyndication(postId: number) {
  await ensureBloggerSyndicationSchema();
  const [rows] = await getDbPool().query<BloggerSyndicationRow[]>(
    `
      SELECT
        post_id,
        external_post_id,
        external_url,
        platform_status,
        source_fingerprint,
        last_error,
        synced_at,
        updated_at
      FROM marketing_blog_syndications
      WHERE post_id = ? AND platform = 'blogger'
      LIMIT 1
    `,
    [postId],
  );

  return rows[0] ?? null;
}

function mapPostStatus(row: BloggerSyndicationRow, post: MarketingBlogPost, blogId: string): BloggerPostStatus {
  return {
    editUrl: `https://www.blogger.com/blog/post/edit/${blogId}/${row.external_post_id}`,
    externalPostId: row.external_post_id,
    externalUrl: row.external_url,
    lastError: row.last_error,
    postId: Number(row.post_id),
    sourceChanged: row.source_fingerprint !== sourceFingerprint(post),
    status: row.platform_status,
    syncedAt: row.synced_at?.toISOString() ?? row.updated_at.toISOString(),
  };
}

export async function getBloggerIntegrationStatus(postId?: number | null): Promise<BloggerIntegrationStatus> {
  const config = getBloggerConfig();
  const access = await getGoogleIntegrationAccess();
  const permissionGranted = Boolean(access?.scopes.includes(GOOGLE_BLOGGER_SCOPE));
  let postStatus: BloggerPostStatus | null = null;

  if (config && postId) {
    const [post, syndication] = await Promise.all([
      getMarketingBlogPostById(postId),
      getSyndication(postId),
    ]);

    if (post && syndication) {
      postStatus = mapPostStatus(syndication, post, config.blogId);
    }
  }

  return {
    accountEmail: access?.accountEmail ?? null,
    blogId: config?.blogId ?? null,
    blogName: config?.blogName ?? "오피셜메일 블로그",
    blogUrl: config?.blogUrl ?? null,
    configured: Boolean(config),
    connected: Boolean(access),
    permissionGranted,
    post: postStatus,
  };
}

async function requireBloggerAccess() {
  const config = getBloggerConfig();

  if (!config) {
    throw new Error("blogger-not-configured");
  }

  const access = await getGoogleIntegrationAccess();

  if (!access) {
    throw new Error("blogger-not-connected");
  }

  if (!access.scopes.includes(GOOGLE_BLOGGER_SCOPE)) {
    throw new Error("blogger-permission-required");
  }

  return { access, config };
}

async function saveSyndication(input: {
  externalPostId: string;
  externalUrl: string | null;
  fingerprint: string;
  postId: number;
  status: "draft" | "published";
}) {
  await ensureBloggerSyndicationSchema();
  await getDbPool().query<ResultSetHeader>(
    `
      INSERT INTO marketing_blog_syndications (
        post_id,
        platform,
        external_post_id,
        external_url,
        platform_status,
        source_fingerprint,
        last_error,
        synced_at
      ) VALUES (?, 'blogger', ?, ?, ?, ?, NULL, NOW())
      ON DUPLICATE KEY UPDATE
        external_post_id = VALUES(external_post_id),
        external_url = VALUES(external_url),
        platform_status = VALUES(platform_status),
        source_fingerprint = VALUES(source_fingerprint),
        last_error = NULL,
        synced_at = NOW(),
        updated_at = NOW()
    `,
    [input.postId, input.externalPostId, input.externalUrl, input.status, input.fingerprint],
  );
}

async function markSyndicationFailed(postId: number, message: string) {
  await ensureBloggerSyndicationSchema();
  await getDbPool().query(
    `
      UPDATE marketing_blog_syndications
      SET platform_status = 'failed', last_error = ?, updated_at = NOW()
      WHERE post_id = ? AND platform = 'blogger'
    `,
    [message.slice(0, 2000), postId],
  );
}

export async function syncBloggerDraft(postId: number) {
  const post = await getMarketingBlogPostById(postId);

  if (!post || post.status !== "published") {
    throw new Error("blogger-source-not-published");
  }

  const { access, config } = await requireBloggerAccess();
  const existing = await getSyndication(postId);
  const body = bloggerPayload(post, config.sourceSiteUrl);

  try {
    const payload = existing
      ? await bloggerRequest<BloggerPostResponse>(
        access.accessToken,
        `/blogs/${encodeURIComponent(config.blogId)}/posts/${encodeURIComponent(existing.external_post_id)}`,
        { method: "PATCH", body: JSON.stringify(body) },
      )
      : await bloggerRequest<BloggerPostResponse>(
        access.accessToken,
        `/blogs/${encodeURIComponent(config.blogId)}/posts?isDraft=true`,
        { method: "POST", body: JSON.stringify(body) },
      );
    const externalPostId = String(payload.id ?? existing?.external_post_id ?? "");

    if (!externalPostId) {
      throw new Error("Blogger가 게시물 ID를 반환하지 않았습니다.");
    }

    const status = payload.status === "LIVE" || existing?.platform_status === "published"
      ? "published"
      : "draft";
    await saveSyndication({
      externalPostId,
      externalUrl: typeof payload.url === "string" ? payload.url : existing?.external_url ?? null,
      fingerprint: sourceFingerprint(post),
      postId,
      status,
    });

    return getBloggerIntegrationStatus(postId);
  } catch (error) {
    await markSyndicationFailed(postId, error instanceof Error ? error.message : "Blogger 동기화에 실패했습니다.");
    throw error;
  }
}

export async function publishBloggerPost(postId: number) {
  let status = await getBloggerIntegrationStatus(postId);

  if (!status.post || status.post.sourceChanged || status.post.status === "failed") {
    status = await syncBloggerDraft(postId);
  }

  if (!status.post) {
    throw new Error("blogger-draft-not-found");
  }

  if (status.post.status === "published") {
    return status;
  }

  const { access, config } = await requireBloggerAccess();
  const payload = await bloggerRequest<BloggerPostResponse>(
    access.accessToken,
    `/blogs/${encodeURIComponent(config.blogId)}/posts/${encodeURIComponent(status.post.externalPostId)}/publish`,
    { method: "POST" },
  );
  const sourcePost = await getMarketingBlogPostById(postId);

  if (!sourcePost) {
    throw new Error("blogger-source-not-published");
  }

  await saveSyndication({
    externalPostId: status.post.externalPostId,
    externalUrl: typeof payload.url === "string" ? payload.url : status.post.externalUrl,
    fingerprint: sourceFingerprint(sourcePost),
    postId,
    status: "published",
  });

  return getBloggerIntegrationStatus(postId);
}
