import type { Token } from '../core/types';
import type { Breakpoint } from '../breakpoints/types';

import { joinTokens } from '../core/joinTokens';
import { measureSliceWidthCached } from '../measure/measureSliceWidthCached';
import { buildTieKey, scoreLayout } from '../scoring/index';

import { approxWidth, DEFAULT_TOO_LONG_THRESHOLDS, isLineStartPunctTC, isTooLong } from './constraints';
import { insertBeamTopK } from './topK';
import type { BestWidgetLayout, SearchWidgetConfig, SearchWidgetInput, SearchWidgetResult, WidgetBeam } from './types';

const DEFAULT_CONFIG: SearchWidgetConfig = {
  beamK: 5,
  expandM: 12,
  tooLongThresholds: DEFAULT_TOO_LONG_THRESHOLDS,
};

function mergeConfig(overrides?: Partial<SearchWidgetConfig> | null): SearchWidgetConfig {
  if (!overrides) return { ...DEFAULT_CONFIG };
  return {
    beamK: overrides.beamK ?? DEFAULT_CONFIG.beamK,
    expandM: overrides.expandM ?? DEFAULT_CONFIG.expandM,
    tooLongThresholds: overrides.tooLongThresholds ?? DEFAULT_CONFIG.tooLongThresholds,
  };
}

function uniqueSortedPositions(bps: Breakpoint[], n: number): number[] {
  const set = new Set<number>();
  for (const b of bps) {
    const p = b.pos | 0;
    if (p >= 1 && p <= n - 1) set.add(p);
  }
  const arr = Array.from(set);
  arr.sort((a, b) => a - b);
  return arr;
}

function rawSeparatorsForLang(tokensLen: number, lang: 'TC' | 'EN'): string[] | undefined {
  if (lang !== 'TC') return undefined;
  // TC：默认拼接不插入额外空格；空格 token 自己决定展示
  return Array.from({ length: Math.max(0, tokensLen) }, () => '');
}

function buildLineText(tokens: Token[], start: number, end: number, rawSeparators?: string[]): string {
  return joinTokens(tokens, start, end, rawSeparators);
}

function mergeApproxReason(a?: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED', b?: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED') {
  if (a === 'MEASURE_FAILED' || b === 'MEASURE_FAILED') return 'MEASURE_FAILED';
  if (a === 'WIDTH_UNKNOWN' || b === 'WIDTH_UNKNOWN') return 'WIDTH_UNKNOWN';
  return undefined;
}

function buildBestLayout(best: WidgetBeam, debug?: boolean): BestWidgetLayout {
  const lines = best.lines.map((l) => l.text);
  const wrappedText = lines.join('\n');

  const reason = best.lines.reduce<'WIDTH_UNKNOWN' | 'MEASURE_FAILED' | undefined>((acc, l) => {
    return mergeApproxReason(acc, l.approxReason);
  }, undefined);

  const meta = debug
    ? { breaks: best.breaks, scoreTopTerms: best.scoreTopTerms, score: best.score, reason }
    : { breaks: best.breaks, reason };

  return { breaks: best.breaks, lines, wrappedText, meta };
}

export async function searchBestLayoutWidget(input: SearchWidgetInput): Promise<SearchWidgetResult> {
  const cfg = mergeConfig(input.config ?? null);
  const tokens = input.tokens ?? [];
  const lang = input.lang;
  const n = tokens.length;

  if (isTooLong(tokens, lang, cfg.tooLongThresholds)) {
    return { ok: false, reason: 'TOO_LONG', meta: { tokenCount: n } };
  }

  const maxLines = Math.max(1, input.maxLines | 0);
  const availableWidth = input.availableWidth;

  const positions = uniqueSortedPositions(input.breakpoints ?? [], n);
  const rawSep = rawSeparatorsForLang(tokens.length, lang);

  const tcPunctuations = input.scoring.config.tcPunctuations ?? ['，', '。', '！', '？', '；', '：', '、'];

  // 初始 beams
  let beams: WidgetBeam[] = [
    { pos: 0, breaks: [], lines: [], score: 0, tieKey: [0, 0, 0, 0, 0] },
  ];

  for (let lineIndex = 1; lineIndex <= maxLines; lineIndex++) {
    let newBeams: WidgetBeam[] = [];

    // 关键：保留已完成（pos==N）的 beams，避免“提前完成的解”在后续轮次被丢弃
    // 否则会错误地倾向“凑满 maxLines 行”的解，造成断句很怪（例如把“村莊”拆开）
    for (const b of beams) {
      if (b.pos === n) {
        newBeams = insertBeamTopK(newBeams, b, cfg.beamK);
      }
    }

    // beams 顺序固定：按当前排序后的顺序扩展
    //（insertBeamTopK 会保持排序；这里再 sort 一次防御）
    beams = beams.slice().sort((a, b) => {
      if (a.score !== b.score) return b.score - a.score;
      const na = a.tieKey;
      const nb = b.tieKey;
      const m = Math.min(na.length, nb.length);
      for (let i = 0; i < m; i++) {
        const av = na[i] ?? 0;
        const bv = nb[i] ?? 0;
        if (av < bv) return -1;
        if (av > bv) return 1;
      }
      return na.length - nb.length;
    });

    for (const beam of beams) {
      const pos = beam.pos;
      if (pos >= n) continue;

      // nextPos：pos 升序 + N
      const nextList: number[] = [];
      for (const p of positions) if (p > pos) nextList.push(p);
      if (n > pos) nextList.push(n);

      // expandM 裁剪：只取前 M 个（确定性：按 pos 升序），但必须保证 N（结束边界）始终可选
      // 否则会出现“明明一行就能结束，却被裁剪掉只能继续拆分”的怪异换行。
      const M = Math.max(1, cfg.expandM | 0);
      let trimmed: number[];
      if (M === 1) {
        trimmed = [n];
      } else {
        const withoutN = nextList.filter((x) => x !== n);
        trimmed = withoutN.slice(0, M - 1);
        trimmed.push(n);
      }

      for (const nextPos of trimmed) {
        if (nextPos <= pos) continue; // 禁止空行

        // H6：TC 禁止行首标点（断点导致下一行第一个 token 为标点）
        if (lang === 'TC' && nextPos < n && isLineStartPunctTC(tokens, nextPos, tcPunctuations)) {
          continue;
        }

        const tokenCount = nextPos - pos;
        const charCount = lang === 'TC' ? tokenCount : 0;
        const lineText = buildLineText(tokens, pos, nextPos, rawSep);

        let lineWidth: number;
        let isApprox = false;
        let approxReason: 'WIDTH_UNKNOWN' | 'MEASURE_FAILED' | undefined;
        let widthTrusted = false;

        if (input.measure.widthMode === 'APPROX') {
          isApprox = true;
          approxReason = 'WIDTH_UNKNOWN';
          lineWidth = approxWidth(tokenCount, charCount, lang);
        } else {
          const widgetEnableMeasure = input.measure.widgetEnableMeasure ?? true;
          const res = await measureSliceWidthCached({
            tokens,
            start: pos,
            end: nextPos,
            context: 'WIDGET',
            contextProfile: input.measure.contextProfile,
            fontSpec: input.measure.fontSpec,
            measureWidthImpl: input.measure.measureWidthImpl,
            widgetEnableMeasure,
            rawSeparators: rawSep,
          });

          if (res.width === null) {
            isApprox = true;
            approxReason = res.meta.reason ?? 'WIDTH_UNKNOWN';
            lineWidth = approxWidth(tokenCount, charCount, lang);
          } else {
            widthTrusted = true;
            lineWidth = res.width;
          }
        }

        // 超宽过滤：仅当宽度可信时执行
        if (widthTrusted && lineWidth > availableWidth) continue;

        const newLine = {
          start: pos,
          end: nextPos,
          text: lineText,
          width: lineWidth,
          tokenCount,
          charCount,
          isApprox,
          approxReason,
        };

        const newLines = beam.lines.concat([newLine]);
        const newBreaks = nextPos === n ? beam.breaks : beam.breaks.concat([nextPos]);
        const layoutCandidate = { breaks: newBreaks, lines: newLines.map(({ isApprox: _a, approxReason: _b, ...rest }) => rest) };

        const scored = scoreLayout({
          tokens,
          layoutCandidate: layoutCandidate as any,
          lang,
          context: 'WIDGET',
          availableWidth,
          config: input.scoring.config,
          lexicons: input.scoring.lexicons,
          debug: Boolean(input.scoring.debug),
        });

        const tieKey = buildTieKey({
          scoredLayout: scored as any,
          layoutCandidate: layoutCandidate as any,
          lang,
          context: 'WIDGET',
          tokenCount: n,
          availableWidth,
          idealWidthRatio: input.scoring.config.idealWidthRatio,
        }) as number[];

        const cand: WidgetBeam = {
          pos: nextPos,
          breaks: newBreaks,
          lines: newLines,
          score: scored.score,
          tieKey,
          scoreTopTerms: scored.scoreBreakdown?.terms,
        };

        newBeams = insertBeamTopK(newBeams, cand, cfg.beamK);
      }
    }

    beams = newBeams;
    if (beams.length === 0) break;
  }

  const finished = beams.filter((b) => b.pos === n);
  if (finished.length === 0) {
    return { ok: false, reason: 'NO_CANDIDATE', meta: { tokenCount: n } };
  }

  finished.sort((a, b) => {
    if (a.score !== b.score) return b.score - a.score;
    const na = a.tieKey;
    const nb = b.tieKey;
    const m = Math.min(na.length, nb.length);
    for (let i = 0; i < m; i++) {
      const av = na[i] ?? 0;
      const bv = nb[i] ?? 0;
      if (av < bv) return -1;
      if (av > bv) return 1;
    }
    return na.length - nb.length;
  });

  const best = finished[0]!;
  return { ok: true, bestLayout: buildBestLayout(best, Boolean(input.scoring.debug)) };
}

