/**
 * Readability ベースのクリーン本文抽出ヘルパー。
 *
 * WebFetch が取得した生 HTML を「記事本文だけ」のクリーンな Markdown に変換する。
 * ナビ・広告・ヘッダー・フッター等のボイラープレートを除去し、見出し・リスト・
 * テーブル・リンクといった構造を保ったまま Markdown 化する。
 *
 * パイプライン: HTML → linkedom で DOM 化 → @mozilla/readability で記事抽出
 *            → turndown(+gfm) で HTML→Markdown 変換。
 *
 * 抽出に失敗（非記事ページ・SPA・空など）したら null を返す。呼び出し側は
 * 既存の素朴な htmlToText にフォールバックして WebFetch を絶対に hard-fail させない。
 *
 * 注意: これはヘルパーモジュールであり、ツールではない（TOOL_DEFS を持たない）。
 *
 * ESM/CJS 注意: turndown は CJS-main / turndown-plugin-gfm も CJS-main。
 * ESM ビルド（"type":"module"）で `not a constructor` 系の罠を踏まないよう、
 * default / named import を明示し、dist 実機での import を検証済み
 * （@kenjiuno/msgreader インシデントの教訓）。
 */

import { Readability } from '@mozilla/readability';
import { parseHTML } from 'linkedom';
import TurndownService from 'turndown';
import { gfm } from 'turndown-plugin-gfm';
import { logger } from '../../../logger.js';

/** クリーン抽出の結果 */
export interface ReadableExtraction {
  /** 記事タイトル（取得できれば） */
  title?: string;
  /** クリーンな Markdown 本文（全文・未切り詰め） */
  markdown: string;
}

/**
 * turndown インスタンスは生成コストがあるので使い回す。
 * - headingStyle: atx（`#` 記法）
 * - codeBlockStyle: fenced（``` 記法）
 * - gfm プラグインでテーブル・打消し線・タスクリストを Markdown 化
 */
let _turndown: TurndownService | null = null;
function getTurndown(): TurndownService {
  if (_turndown) return _turndown;
  const td = new TurndownService({
    headingStyle: 'atx',
    codeBlockStyle: 'fenced',
    bulletListMarker: '-',
    hr: '---',
  });
  td.use(gfm);
  // 画像は alt テキスト + URL を残す（agent が参照できるように）
  _turndown = td;
  return _turndown;
}

/**
 * 生 HTML から記事本文を抽出してクリーンな Markdown を返す。
 * 抽出できなければ null（呼び出し側で htmlToText にフォールバック）。
 *
 * @param html 生 HTML 文字列
 * @param opts.baseUrl 相対リンク解決用の URL（任意）。Markdown 内の相対リンクを絶対化する
 * @param opts.selector CSS セレクタ（任意）。指定すると、その要素のサブツリーだけを
 *        クリーン抽出する。要素が見つからなければ null（呼び出し側でフォールバック）。
 */
export function extractReadableMarkdown(
  html: string,
  opts: { baseUrl?: string; selector?: string } = {},
): ReadableExtraction | null {
  if (!html || html.trim().length === 0) return null;
  const { baseUrl, selector } = opts;

  let articleHtml: string;
  let title: string | undefined;
  try {
    // linkedom で DOM を構築。
    const { document } = parseHTML(html);

    // selector 指定時は、その要素サブツリーを直接 Markdown 化する。
    if (selector) {
      let el: Element | null = null;
      try {
        el = document.querySelector(selector) as unknown as Element | null;
      } catch {
        el = null; // 不正なセレクタは無視（フォールバックさせる）
      }
      if (!el) return null;
      const inner = (el as unknown as { innerHTML?: string }).innerHTML ?? '';
      if (!inner.trim()) return null;
      articleHtml = inner;
      const docTitle = (document as unknown as { title?: string }).title;
      title = docTitle?.trim() || undefined;
    } else {
      const reader = new Readability(document as unknown as Document, {
        // 短い記事でも諦めないよう閾値を下げる（デフォルト 500）
        charThreshold: 250,
      });
      const parsed = reader.parse();
      if (!parsed || !parsed.content || parsed.content.trim().length === 0) {
        return null;
      }
      articleHtml = parsed.content;
      title = parsed.title?.trim() || undefined;
    }
  } catch (e) {
    logger.debug(`[readability] extraction failed: ${(e as Error).message}`);
    return null;
  }

  try {
    let markdown = getTurndown().turndown(articleHtml).trim();
    if (markdown.length === 0) return null;
    // baseUrl があれば、Markdown 内のルート相対 / プロトコル相対リンクを絶対化する。
    if (baseUrl) markdown = absolutizeLinks(markdown, baseUrl);
    return { ...(title ? { title } : {}), markdown };
  } catch (e) {
    logger.debug(`[readability] turndown conversion failed: ${(e as Error).message}`);
    return null;
  }
}

/**
 * Markdown 内の相対リンク `](/path)` `](path)` を baseUrl 基準で絶対化する。
 * 既に絶対 (http/https/mailto/data 等) や アンカー (#) のものはそのまま。
 */
function absolutizeLinks(markdown: string, baseUrl: string): string {
  let base: URL;
  try {
    base = new URL(baseUrl);
  } catch {
    return markdown;
  }
  // Markdown のリンク / 画像の URL 部分のみ書き換える: ](URL) と ](URL "title")
  return markdown.replace(/(\]\()([^)\s]+)(\s+"[^"]*")?(\))/g, (_m, open, url, titlePart, close) => {
    let abs = url;
    if (!/^[a-z][a-z0-9+.-]*:/i.test(url) && !url.startsWith('#')) {
      try {
        abs = new URL(url, base).toString();
      } catch {
        abs = url;
      }
    }
    return `${open}${abs}${titlePart ?? ''}${close}`;
  });
}

/**
 * agent のコンテキストに返すために markdown を上限文字数で切り詰める。
 * 切り詰めた場合は note を付与（source 保存済みなら full 版の在処を案内）。
 *
 * @param markdown 全文クリーン Markdown
 * @param limit 上限文字数
 * @param savedSourceFile source/ に保存された相対ファイル名（あれば note に含める）
 */
export function truncateForAgent(
  markdown: string,
  limit: number,
  savedSourceFile?: string | null,
): string {
  if (markdown.length <= limit) return markdown;
  const note = savedSourceFile
    ? `\n\n... (truncated; full version saved to source/${savedSourceFile})`
    : '\n\n... (truncated)';
  return markdown.slice(0, limit) + note;
}
