const FEISHU_API_ORIGIN = "https://open.feishu.cn";
const MAX_SHEET_ROWS = 5_000;
const MAX_SHEET_COLUMNS = 100;
const MAX_CONTENT_ROWS = 1_000;
const DEFAULT_MAX_MEDIA_BYTES = 200_000_000;

export type FeishuBindings = {
  FEISHU_APP_ID?: string;
  FEISHU_APP_SECRET?: string;
};

export type FeishuSourceImage = {
  index: number;
  fileToken: string;
  width: number | null;
  height: number | null;
};

export type FeishuSourceVideo = {
  index: number;
  fileToken: string;
  name: string;
  mimeType: string;
  size: number | null;
};

export type FeishuSourceRow = {
  sourceRow: number;
  title: string;
  body: string;
  images: FeishuSourceImage[];
  videos: FeishuSourceVideo[];
};

export type FeishuSource = {
  url: string;
  wikiToken: string;
  spreadsheetToken: string;
  sheetId: string;
  sheetName: string;
  syncedAt: string;
  columns: string[];
  rows: FeishuSourceRow[];
};

type FetchLike = typeof fetch;

type FeishuEnvelope<T> = {
  code?: number;
  msg?: string;
  data?: T;
  tenant_access_token?: string;
  expire?: number;
};

type SheetInfo = {
  sheet_id?: string;
  title?: string;
  hidden?: boolean;
  resource_type?: string;
  grid_properties?: {
    row_count?: number;
    column_count?: number;
  };
};

type CachedAccessToken = {
  appId: string;
  token: string;
  expiresAt: number;
};

let cachedAccessToken: CachedAccessToken | null = null;

export class FeishuSourceError extends Error {
  status: number;

  constructor(message: string, status = 500) {
    super(message);
    this.name = "FeishuSourceError";
    this.status = status;
  }
}

function bindingValue(value: unknown) {
  return String(value ?? "").trim();
}

function providerMessage(value: unknown) {
  const message = String(value ?? "").trim();
  return message
    .replace(/https?:\/\/[^\s"'<>]+/gi, "[飞书权限链接]")
    .slice(0, 240);
}

function normalizeHeader(value: unknown) {
  return cellText(value)
    .toLowerCase()
    .replace(/[\s（）()【】[\]_\-—:：/\\]+/g, "");
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function cellText(value: unknown): string {
  if (value === null || value === undefined) return "";
  if (typeof value === "string") return value.trim();
  if (typeof value === "number" || typeof value === "boolean") {
    return String(value);
  }
  if (Array.isArray(value)) {
    return value
      .map(cellText)
      .filter(Boolean)
      .join("");
  }
  if (
    !isRecord(value) ||
    value.type === "embed-image" ||
    value.type === "attachment"
  ) return "";
  if (typeof value.text === "string") return value.text.trim();
  if (typeof value.value === "string") return value.value.trim();
  return "";
}

function extractVideos(value: unknown, output: FeishuSourceVideo[]) {
  if (Array.isArray(value)) {
    for (const item of value) extractVideos(item, output);
    return;
  }
  if (!isRecord(value)) return;
  const fileToken = bindingValue(value.fileToken ?? value.file_token);
  const mimeType = bindingValue(value.mimeType ?? value.mime_type);
  const name = bindingValue(value.text ?? value.name ?? value.file_name);
  const isVideo =
    value.type === "attachment" &&
    (mimeType.startsWith("video/") || /\.(?:mp4|mov|m4v|webm)$/i.test(name));
  if (isVideo && fileToken) {
    output.push({
      index: 0,
      fileToken,
      name: name || "视频",
      mimeType: mimeType || "video/mp4",
      size:
        typeof value.size === "number" && Number.isFinite(value.size)
          ? value.size
          : null,
    });
  }
  for (const child of Object.values(value)) {
    if (child !== value.fileToken && child !== value.file_token) {
      extractVideos(child, output);
    }
  }
}

function extractImages(value: unknown, output: FeishuSourceImage[]) {
  if (Array.isArray(value)) {
    for (const item of value) extractImages(item, output);
    return;
  }
  if (!isRecord(value)) return;
  const fileToken = bindingValue(value.fileToken ?? value.image_token);
  if (value.type === "embed-image" && fileToken) {
    output.push({
      index: 0,
      fileToken,
      width:
        typeof value.width === "number"
          ? value.width
          : typeof value.image_width === "number"
            ? value.image_width
            : null,
      height:
        typeof value.height === "number"
          ? value.height
          : typeof value.image_height === "number"
            ? value.image_height
            : null,
    });
  }
  for (const child of Object.values(value)) {
    if (child !== value.fileToken && child !== value.image_token) {
      extractImages(child, output);
    }
  }
}

function columnName(index: number) {
  let value = index + 1;
  let result = "";
  while (value > 0) {
    const remainder = (value - 1) % 26;
    result = String.fromCharCode(65 + remainder) + result;
    value = Math.floor((value - 1) / 26);
  }
  return result;
}

function headerMatches(header: string, candidates: RegExp[]) {
  return candidates.some((candidate) => candidate.test(header));
}

function findHeader(values: unknown[][]) {
  let best:
    | {
        rowIndex: number;
        idIndex: number;
        titleIndex: number;
        bodyIndex: number;
        tagsIndex: number;
        videoIndex: number;
        score: number;
      }
    | undefined;

  for (let rowIndex = 0; rowIndex < Math.min(values.length, 20); rowIndex += 1) {
    const headers = (values[rowIndex] ?? []).map(normalizeHeader);
    const idIndex = headers.findIndex((header) =>
      headerMatches(header, [/^id$/, /作品id/, /序号/, /编号/]),
    );
    const titleIndex = headers.findIndex((header) =>
      headerMatches(header, [/标题/, /题目/]),
    );
    const bodyIndex = headers.findIndex((header) =>
      headerMatches(header, [/笔记内容/, /正文/, /文案/, /^内容$/]),
    );
    const tagsIndex = headers.findIndex((header) =>
      headerMatches(header, [/标签/, /话题/, /^tags?$/]),
    );
    const videoIndex = headers.findIndex((header) =>
      headerMatches(header, [/^视频\d*$/, /视频文件/, /视频素材/]),
    );
    const score =
      (titleIndex >= 0 ? 5 : 0) +
      (bodyIndex >= 0 ? 5 : 0) +
      (videoIndex >= 0 ? 2 : 0) +
      (idIndex >= 0 ? 1 : 0) +
      (tagsIndex >= 0 ? 1 : 0);
    if (!best || score > best.score) {
      best = {
        rowIndex,
        idIndex,
        titleIndex,
        bodyIndex,
        tagsIndex,
        videoIndex,
        score,
      };
    }
  }

  if (!best || best.titleIndex < 0 || best.bodyIndex < 0) {
    throw new FeishuSourceError(
      "没有找到“标题”和“正文/笔记内容”列，请检查飞书表头",
      422,
    );
  }
  return best;
}

function parseRows(values: unknown[][]) {
  const header = findHeader(values);
  const headerRow = values[header.rowIndex] ?? [];
  const usedSourceRows = new Set<number>();
  const rows: FeishuSourceRow[] = [];
  let maxImageCount = 0;
  let maxVideoCount = 0;

  for (
    let rowIndex = header.rowIndex + 1;
    rowIndex < values.length && rows.length < MAX_CONTENT_ROWS;
    rowIndex += 1
  ) {
    const row = values[rowIndex] ?? [];
    const title = cellText(row[header.titleIndex]);
    if (!title) continue;
    const rawBody = cellText(row[header.bodyIndex]);
    const tags =
      header.tagsIndex >= 0 ? cellText(row[header.tagsIndex]) : "";
    const body =
      tags && !rawBody.includes(tags)
        ? [rawBody, tags].filter(Boolean).join("\n\n")
        : rawBody;
    const idValue =
      header.idIndex >= 0
        ? Number.parseInt(cellText(row[header.idIndex]), 10)
        : Number.NaN;
    let sourceRow =
      Number.isInteger(idValue) && idValue > 0 ? idValue : rowIndex + 1;
    if (usedSourceRows.has(sourceRow)) sourceRow = rowIndex + 1;
    usedSourceRows.add(sourceRow);

    const collectedImages: FeishuSourceImage[] = [];
    for (const cell of row) extractImages(cell, collectedImages);
    const seenTokens = new Set<string>();
    const images = collectedImages
      .filter((image) => {
        if (seenTokens.has(image.fileToken)) return false;
        seenTokens.add(image.fileToken);
        return true;
      })
      .map((image, imageIndex) => ({ ...image, index: imageIndex + 1 }));
    maxImageCount = Math.max(maxImageCount, images.length);
    const collectedVideos: FeishuSourceVideo[] = [];
    if (header.videoIndex >= 0) {
      extractVideos(row[header.videoIndex], collectedVideos);
    }
    const seenVideoTokens = new Set<string>();
    const videos = collectedVideos
      .filter((video) => {
        if (seenVideoTokens.has(video.fileToken)) return false;
        seenVideoTokens.add(video.fileToken);
        return true;
      })
      .map((video, videoIndex) => ({ ...video, index: videoIndex + 1 }));
    maxVideoCount = Math.max(maxVideoCount, videos.length);
    rows.push({ sourceRow, title, body, images, videos });
  }

  if (rows.length === 0) {
    throw new FeishuSourceError("表格中没有可导入的有效标题行", 422);
  }

  const matchedColumns = [
    header.idIndex >= 0 ? cellText(headerRow[header.idIndex]) : "",
    cellText(headerRow[header.titleIndex]) || "标题",
    header.tagsIndex >= 0 ? cellText(headerRow[header.tagsIndex]) : "",
    cellText(headerRow[header.bodyIndex]) || "正文",
    ...Array.from({ length: maxImageCount }, (_, index) => `图片${index + 1}`),
    ...Array.from({ length: maxVideoCount }, (_, index) => `视频${index + 1}`),
  ].filter(Boolean);

  return {
    columns: [...new Set(matchedColumns)],
    rows,
  };
}

async function jsonEnvelope<T>(
  response: Response,
  fallbackMessage: string,
): Promise<FeishuEnvelope<T>> {
  const text = await response.text();
  let payload: FeishuEnvelope<T>;
  try {
    payload = JSON.parse(text) as FeishuEnvelope<T>;
  } catch {
    throw new FeishuSourceError(
      `${fallbackMessage}（飞书返回了异常响应）`,
      502,
    );
  }
  if (!response.ok || (typeof payload.code === "number" && payload.code !== 0)) {
    const code = Number(payload.code);
    const rawMessage = providerMessage(payload.msg);
    if ([99991672, 99991679].includes(code)) {
      throw new FeishuSourceError(
        "飞书应用缺少电子表格或知识库读取权限，请先在飞书开放平台开通权限",
        403,
      );
    }
    if ([131006, 1310213].includes(code) || response.status === 403) {
      throw new FeishuSourceError(
        "飞书应用没有这张表的访问权限，请在表格中添加该文档应用或将应用加入知识库",
        403,
      );
    }
    if ([131005, 1310214].includes(code) || response.status === 404) {
      throw new FeishuSourceError("没有找到对应的飞书表格", 404);
    }
    throw new FeishuSourceError(
      rawMessage
        ? `${fallbackMessage}：${rawMessage}`
        : `${fallbackMessage}（HTTP ${response.status}）`,
      response.status >= 400 ? response.status : 502,
    );
  }
  return payload;
}

function feishuConfig(bindings: FeishuBindings) {
  const appId = bindingValue(bindings.FEISHU_APP_ID);
  const appSecret = bindingValue(bindings.FEISHU_APP_SECRET);
  if (!appId || !appSecret) {
    throw new FeishuSourceError(
      "飞书 API 尚未配置，请先配置应用的 App ID 和 App Secret",
      503,
    );
  }
  return { appId, appSecret };
}

async function accessToken(
  bindings: FeishuBindings,
  fetchImpl: FetchLike,
) {
  const { appId, appSecret } = feishuConfig(bindings);
  if (
    cachedAccessToken?.appId === appId &&
    cachedAccessToken.expiresAt > Date.now() + 60_000
  ) {
    return cachedAccessToken.token;
  }
  const response = await fetchImpl(
    `${FEISHU_API_ORIGIN}/open-apis/auth/v3/tenant_access_token/internal`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json; charset=utf-8" },
      body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
      signal: AbortSignal.timeout(12_000),
    },
  );
  const payload = await jsonEnvelope<never>(response, "获取飞书访问凭证失败");
  const token = bindingValue(payload.tenant_access_token);
  if (!token) {
    throw new FeishuSourceError("飞书没有返回有效访问凭证", 502);
  }
  cachedAccessToken = {
    appId,
    token,
    expiresAt: Date.now() + Math.max(300, Number(payload.expire) || 7_200) * 1_000,
  };
  return token;
}

async function feishuGet<T>(
  path: string,
  token: string,
  fetchImpl: FetchLike,
  fallbackMessage: string,
  params?: URLSearchParams,
) {
  const url = new URL(path, FEISHU_API_ORIGIN);
  if (params) url.search = params.toString();
  const response = await fetchImpl(url.toString(), {
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json; charset=utf-8",
    },
    signal: AbortSignal.timeout(15_000),
  });
  const payload = await jsonEnvelope<T>(response, fallbackMessage);
  if (!payload.data) {
    throw new FeishuSourceError(`${fallbackMessage}（响应缺少数据）`, 502);
  }
  return payload.data;
}

function parseSourceUrl(input: string) {
  let url: URL;
  try {
    url = new URL(input);
  } catch {
    throw new FeishuSourceError("飞书链接格式无效", 400);
  }
  const validHost =
    url.hostname === "feishu.cn" ||
    url.hostname.endsWith(".feishu.cn") ||
    url.hostname === "larksuite.com" ||
    url.hostname.endsWith(".larksuite.com");
  if (!["http:", "https:"].includes(url.protocol) || !validHost) {
    throw new FeishuSourceError("请粘贴飞书 Wiki 或电子表格链接", 400);
  }
  const match = url.pathname.match(/\/(wiki|sheets|spreadsheets)\/([^/?]+)/);
  if (!match) {
    throw new FeishuSourceError("请粘贴飞书 Wiki 或电子表格链接", 400);
  }
  return {
    url,
    kind: match[1],
    token: match[2],
    requestedSheetId: bindingValue(url.searchParams.get("sheet")),
  };
}

export async function readFeishuSource(
  input: string,
  bindings: FeishuBindings,
  fetchImpl: FetchLike = fetch,
): Promise<FeishuSource> {
  const parsed = parseSourceUrl(input);
  const token = await accessToken(bindings, fetchImpl);
  let wikiToken = "";
  let spreadsheetToken = parsed.token;

  if (parsed.kind === "wiki") {
    wikiToken = parsed.token;
    const nodeData = await feishuGet<{
      node?: { obj_type?: string; obj_token?: string };
    }>(
      "/open-apis/wiki/v2/spaces/get_node",
      token,
      fetchImpl,
      "读取飞书知识库节点失败",
      new URLSearchParams({ token: parsed.token }),
    );
    if (nodeData.node?.obj_type !== "sheet" || !nodeData.node.obj_token) {
      throw new FeishuSourceError("该飞书 Wiki 链接不是电子表格", 422);
    }
    spreadsheetToken = nodeData.node.obj_token;
  }

  const sheetData = await feishuGet<{ sheets?: SheetInfo[] }>(
    `/open-apis/sheets/v3/spreadsheets/${encodeURIComponent(spreadsheetToken)}/sheets/query`,
    token,
    fetchImpl,
    "读取飞书工作表列表失败",
  );
  const visibleSheets = (sheetData.sheets ?? []).filter(
    (sheet) =>
      sheet.sheet_id &&
      sheet.resource_type !== "bitable" &&
      sheet.hidden !== true,
  );
  let selectedSheet = parsed.requestedSheetId
    ? visibleSheets.find((sheet) => sheet.sheet_id === parsed.requestedSheetId)
    : undefined;
  if (!parsed.requestedSheetId) {
    if (visibleSheets.length === 1) {
      selectedSheet = visibleSheets[0];
    } else if (visibleSheets.length > 1) {
      throw new FeishuSourceError(
        "该表格包含多个工作表，请打开目标工作表后复制带 sheet 参数的链接",
        422,
      );
    }
  }
  if (!selectedSheet?.sheet_id) {
    throw new FeishuSourceError("链接中的工作表不存在或已隐藏", 404);
  }

  const rowCount = Math.max(
    1,
    Math.min(
      MAX_SHEET_ROWS,
      Number(selectedSheet.grid_properties?.row_count) || 200,
    ),
  );
  const columnCount = Math.max(
    1,
    Math.min(
      MAX_SHEET_COLUMNS,
      Number(selectedSheet.grid_properties?.column_count) || 26,
    ),
  );
  const range = `${selectedSheet.sheet_id}!A1:${columnName(columnCount - 1)}${rowCount}`;
  const valuesParams = new URLSearchParams();
  valuesParams.append("ranges", range);
  const valuesData = await feishuGet<{
    valueRanges?: Array<{ values?: unknown[][] }>;
  }>(
    `/open-apis/sheets/v2/spreadsheets/${encodeURIComponent(spreadsheetToken)}/values_batch_get`,
    token,
    fetchImpl,
    "读取飞书表格内容失败",
    valuesParams,
  );
  const values = valuesData.valueRanges?.[0]?.values;
  if (!Array.isArray(values)) {
    throw new FeishuSourceError("飞书表格没有返回可读取的单元格", 422);
  }
  const parsedRows = parseRows(values);

  return {
    url: parsed.url.toString(),
    wikiToken,
    spreadsheetToken,
    sheetId: selectedSheet.sheet_id,
    sheetName: bindingValue(selectedSheet.title) || "未命名工作表",
    syncedAt: new Date().toISOString(),
    columns: parsedRows.columns,
    rows: parsedRows.rows,
  };
}

export async function downloadFeishuMedia(
  fileToken: string,
  bindings: FeishuBindings,
  fetchImpl: FetchLike = fetch,
  options: { maxBytes?: number; label?: string; timeoutMs?: number } = {},
) {
  const maxBytes = Math.max(1, options.maxBytes ?? DEFAULT_MAX_MEDIA_BYTES);
  const label = bindingValue(options.label) || "素材";
  const normalizedToken = bindingValue(fileToken);
  if (!/^[A-Za-z0-9_-]{8,240}$/.test(normalizedToken)) {
    throw new FeishuSourceError(`飞书${label}标识无效`, 400);
  }
  const token = await accessToken(bindings, fetchImpl);
  const response = await fetchImpl(
    `${FEISHU_API_ORIGIN}/open-apis/drive/v1/medias/${encodeURIComponent(normalizedToken)}/download`,
    {
      headers: { Authorization: `Bearer ${token}` },
      signal: AbortSignal.timeout(options.timeoutMs ?? 120_000),
    },
  );
  const declaredSize = Number(response.headers.get("content-length"));
  if (Number.isFinite(declaredSize) && declaredSize > maxBytes) {
    throw new FeishuSourceError(`飞书${label}文件过大，无法同步`, 413);
  }
  if (!response.ok) {
    throw new FeishuSourceError(
      response.status === 403
        ? `飞书应用没有这个${label}的下载权限`
        : `下载飞书${label}失败（HTTP ${response.status}）`,
      response.status === 403 ? 403 : 502,
    );
  }
  const bytes = await response.arrayBuffer();
  if (bytes.byteLength > maxBytes) {
    throw new FeishuSourceError(`飞书${label}文件过大，无法同步`, 413);
  }
  return {
    bytes,
    contentType:
      response.headers.get("content-type")?.split(";")[0] ||
      "application/octet-stream",
  };
}

export function clearFeishuAccessTokenCacheForTests() {
  cachedAccessToken = null;
}
