#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""am STE 文字规整器 —— 把引擎产的散文 md 改成 am 插件吃得下的短句形态。

背景（2026-10-23 实测）：
  am 的 STE 检查对「步骤」（编号列表）限 35 字，对普通句子限 45 字，
  每段句子数上限 6；句子边界只认 。；！？——逗号不算。
  引擎产的复盘/早盘 md 里消息面每条 60-119 字、场景条件链 80 字，必然命中报警。

本模块只做文字层面的等价改写，不动事实、不删数字：
  1. 编号列表 `1. xxx` → `- xxx`（35 字步骤上限 → 45 字列表上限）
  2. 长句按 ，/：/、 逐级断开，断点用 ；连接，每段 ≤ limit
  3. 单段句子数 > 6 时插入空行（STE paragraph-length 上限 6 句）
  4. 表格单元格超长 → 用 <br> 换行（单元格内 <br> 保留）
  5. `callout` 围栏内的正文同样规整（其他围栏 flow/kv/limits/chart/code 一律不动）
  6. 词级规整：`X以内`→`不超过X`、`X以上`→`不低于X`、`进行优化`→`优化`、
     `综上所述`/`赋能` 删除

幂等：跑第二遍不再改动。
"""
import re

PROSE_LIMIT = 45      # 描述句上限（实测 max 45）
MAX_SENT = 6          # 每段句子数上限（实测 max 6）

_SENT_RE = re.compile(r'[^。；！？!?;]+[。；！？!?;]?')
_NUM_LIST_RE = re.compile(r'^(\s*)\d+\s*[.、．]\s+(.*)$')
_BOLD_ONLY_RE = re.compile(r'^\*\*[^*]+\*\*\s*$')
DELIM_GROUPS = (('，', ','), ('：', ':'), ('、',), ('—',))
_SAFE_SLASH = re.compile(r'(?<!\d)[/／](?!\d)')   # 不切 10/19 这类日期
SLASH_MARK = '\u0001'
_PAREN_RE = re.compile(r'[（(]([^（()）]{20,})[)）]')
CELL_SPLIT_SEPS = (' 且 ', ' 或 ', '；', ';')


def _clen(s):
    """按字符数计长度（中文按 1 字算，与 am 的判定一致）。"""
    return len(s)


OPENERS = '（(「【[《'
CLOSERS = '）)」】]》'


def _atoms(body, delims):
    """切分为原子块；括号内的定界符不作为切点（避免把 (昨55,-46) 切开）。"""
    out, cur, depth = [], '', 0
    for ch in body:
        cur += ch
        if ch in OPENERS:
            depth += 1
        elif ch in CLOSERS:
            depth = max(0, depth - 1)
        elif depth == 0 and ch in delims:
            out.append((cur, True))
            cur = ''
    if cur:
        out.append((cur, False))
    return out


def _greedy(body, delims, limit):
    """按一组标点切分后贪心合并成 ≤limit 的块（只在顶层定界符处断）。"""
    cls = ''.join(re.escape(d) for d in delims)
    if not re.search(f'[{cls}]', body):
        return [body]
    chunks, cur = [], ''
    for atom, splittable in _atoms(body, delims):
        bare = atom.rstrip(cls)
        if _clen(cur + bare) <= limit:
            cur += atom
        else:
            if cur.strip():
                chunks.append(cur.rstrip(cls))
            cur = atom
    if cur.strip():
        chunks.append(cur.rstrip(cls))
    return [c.strip() for c in chunks if c.strip()]


def _split_frags(body, limit, extra_groups=()):
    """把一句超长中文切成若干 ≤limit 的片段。逗号 → 冒号 → 顿号逐级降级。"""
    if _clen(body) <= limit:
        return [body]
    frags = [body]
    for group in tuple(DELIM_GROUPS) + tuple(extra_groups):
        out = []
        for f in frags:
            out.extend(_greedy(f, group, limit) if _clen(f) > limit else [f])
        frags = out
    # 括号内的长数据链（CPO+47.06/光通信+33.53/…）按 / 断开，写在括号里
    out_p = []
    for f in frags:
        if _clen(f) > limit and _PAREN_RE.search(f):
            f = _PAREN_RE.sub(lambda m: _paren_rep(m, limit), f)
            out_p.extend(_greedy(f, ('，', ','), limit) if _clen(f) > limit else [f])
        else:
            out_p.append(f)
    frags = out_p
    # 斜杠链（大业股份3板/福莱新材·宁波东力2板…）：安全斜杠与间隔号作最后降级
    out_s = []
    for f in frags:
        if _clen(f) > limit:
            marked = _SAFE_SLASH.sub(SLASH_MARK, f)
            for part in _greedy(marked, (SLASH_MARK, '·', '→', '+'), limit):
                fixed = re.sub(r'\s*[；]\s*', '；', part.replace(SLASH_MARK, '；').replace('·', '；'))
                out_s.append(fixed)
        else:
            out_s.append(f)
    frags = out_s
    # 最后一档：连括号内也断开（仍是等价改写，只是标点进了括号）
    out = []
    for f in frags:
        if _clen(f) > limit:
            cls = '、,，'
            chunks, cur = [], ''
            for ch in f:
                cur += ch
                if ch in cls:
                    chunks.append(cur)
                    cur = ''
            if cur:
                chunks.append(cur)
            chunks = [c.strip() for c in chunks if c.strip()]
            merged, cur2 = [], ''
            for c in chunks:
                if _clen(cur2 + c.rstrip(cls)) <= limit:
                    cur2 += c
                else:
                    if cur2.strip():
                        merged.append(cur2.rstrip(cls))
                    cur2 = c
            if cur2.strip():
                merged.append(cur2.rstrip(cls))
            out.extend(merged or [f])
        else:
            out.append(f)
    return out or [body]


def reflow(text, limit=PROSE_LIMIT):
    """长句 → 多短句（用 ；连接），句子数 >6 时用空行分段。"""
    text = text.strip()
    if not text:
        return text
    sents = []
    for p in _SENT_RE.findall(text):
        term = p[-1] if p and p[-1] in '。；！？!?;' else ''
        body = p[:-1] if term else p
        if not body.strip():
            if term and sents:
                sents[-1] += term
            continue
        frags = [f.strip() for f in _split_frags(body, limit) if f.strip()]
        if len(frags) > 1:
            sents.extend(f'{f}；' for f in frags[:-1])
            sents.append(frags[-1] + (term or '。'))
        else:
            sents.append((frags[0] if frags else body.strip()) + (term or ''))
    if len(sents) > MAX_SENT:
        return '\n\n'.join(''.join(sents[i:i + MAX_SENT]) for i in range(0, len(sents), MAX_SENT))
    return ''.join(sents)


def _fix_words(s):
    """词级规整（STE 词表里被点名的写法）。"""
    s = re.sub(r'\s*[;；]\s*', '；', s)          # 分号统一成全角并去掉两侧空格（幂等）
    guard = r'(?<![\d一二三四五六七八九十百千.．])'
    s = re.sub(guard + r'([一二三四五六七八九十百千\d][^\s，。；、,]{0,3})以内', r'不超过\1', s)
    s = re.sub(guard + r'([一二三四五六七八九十百千\d][^\s，。；、,]{0,3})以上', r'不低于\1', s)
    s = s.replace('进行优化', '优化').replace('进行跟踪', '跟踪').replace('进行分析', '分析')
    s = s.replace('显著提升', '明显提升').replace('缺省', '默认').replace('与此同时，', '同时，').replace('与此同时', '同时')
    s = s.replace('综上所述，', '').replace('综上所述', '').replace('赋能', '')
    return s


def _paren_rep(m, limit):
    """把括号内的 / 分隔数据链拆成若干 ≤limit 的段，段间用 ；。"""
    inner = m.group(1)
    seps = re.findall(r'[、，,:：/+]', inner)
    if len(seps) < 2:
        return m.group(0)
    merged, cur = [], ''
    for piece in [p for p in re.split(r'[、，,:：/+]', inner) if p]:
        if _clen(cur + piece) + 1 <= limit - 2:
            cur = f'{cur}/{piece}' if cur else piece
        else:
            if cur:
                merged.append(cur)
            cur = piece
    if cur:
        merged.append(cur)
    if len(merged) <= 1:
        return m.group(0)
    return '（' + '；'.join(merged) + '）'


def _cell_split(cell, limit):
    """表格单元格超长 → 用 ；断句（实测 <br> 不算句子边界，；才算）。"""
    if _clen(cell) <= limit:
        return cell
    parts = [cell]
    for sep in CELL_SPLIT_SEPS:
        nxt = []
        for p in parts:
            nxt.extend([x.strip() for x in p.split(sep) if x.strip()] if sep in p else [p])
        parts = nxt
    pieces = []
    for p in parts:
        # 单元格里 '/' 是并列数据的天然分隔，最后一档降级
        pieces.extend(_split_frags(p, limit, extra_groups=(('/',),)) if _clen(p) > limit else [p])
    if len(pieces) <= 1:
        return cell
    return '；'.join(pieces)


_URL_RE = re.compile(r'https?://[^\s)）\]]+|www\.[^\s)）\]]+|\[[^\]]*\]\([^)]*\)')
PH_OPEN, PH_CLOSE = '\u0002', '\u0003'


def _protect(s):
    """把 URL / markdown 链接换成占位符，避免被当成分隔符切碎。"""
    saved = []

    def rep(m):
        saved.append(m.group(0))
        return f'{PH_OPEN}{len(saved) - 1}{PH_CLOSE}'
    return _URL_RE.sub(rep, s), saved


def _restore(s, saved):
    for i, u in enumerate(saved):
        s = s.replace(f'{PH_OPEN}{i}{PH_CLOSE}', u)
    return s


def _prose_line(line, limit=PROSE_LIMIT):
    """单行正文/列表项 → STE 合规形态。"""
    protected, saved = _protect(line)
    return _restore(_prose_line_inner(protected, limit), saved)


def _prose_line_inner(line, limit=PROSE_LIMIT):
    indent = line[:len(line) - len(line.lstrip())]
    s = _fix_words(line.strip())
    if not s or s.startswith(('#', '>', '![', '<!--')):
        return indent + reflow(s, limit) if s else line
    m = _NUM_LIST_RE.match(s)
    if m:
        s = f'- {m.group(2)}'
    if s.startswith(('- ', '* ')):
        body = s[2:]
        if _BOLD_ONLY_RE.match(body):
            return indent + s
        flowed = reflow(body, limit)
        if '\n\n' in flowed:            # >6 句：拆成多条列表项，避免 paragraph-length
            chunks = [c.strip() for c in flowed.split('\n\n') if c.strip()]
            return '\n'.join(indent + '- ' + c for c in chunks)
        return indent + '- ' + flowed
    return indent + reflow(s, limit)


def _split_dense_paragraphs(lines):
    """连续多行正文合成一段审视：句数 >6 时插空行分段（STE paragraph-length）。"""
    out, buf = [], []

    def flush():
        if not buf:
            return
        text = ''.join(buf)
        sents = _SENT_RE.findall(text)
        if len(sents) <= MAX_SENT:
            out.extend(buf)
        else:
            groups = [''.join(sents[i:i + MAX_SENT]) for i in range(0, len(sents), MAX_SENT)]
            for i, g in enumerate(groups):
                if i:
                    out.append('')
                out.append(g.strip())
        buf.clear()

    for ln in lines:
        st = ln.strip()
        plain = bool(st) and not st.startswith(('#', '|', '-', '*', '>', '```', '!['))
        if plain:
            buf.append(ln)
        else:
            flush()
            out.append(ln)
    flush()
    return out


def normalize(text, prose_limit=PROSE_LIMIT):
    """md 草稿 → STE 合规草稿（幂等）。"""
    out, fence = [], None
    for ln in text.split('\n'):
        st = ln.strip()
        if fence is None and st.startswith('```'):
            fence = st[3:].strip()
            out.append(ln)
            continue
        if fence is not None:
            if st.startswith('```'):
                fence = None
                out.append(ln)
                continue
            out.append(_prose_line(ln, prose_limit) if fence.startswith('callout') else ln)
            continue
        if not st:
            out.append(ln)
            continue
        if st.startswith('|'):
            if re.match(r'^\|[\s:|-]+\|$', st):        # 表格分隔行
                out.append(ln)
                continue
            cells = st.split('|')
            fixed = []
            for idx, c in enumerate(cells):
                c2, saved = _protect(c.strip())
                c2 = _restore(_fix_words(c2), saved)
                if not c2 and idx in (0, len(cells) - 1):
                    fixed.append('')
                else:
                    fixed.append(f' {_cell_split(c2, prose_limit) if c2 else c2} ')
            out.append('|'.join(fixed))
            continue
        if st.startswith('#'):
            out.append(ln)
            continue
        out.append(_prose_line(ln, prose_limit))
    return '\n'.join(_split_dense_paragraphs(out))


if __name__ == '__main__':
    import sys
    src, dst = sys.argv[1], sys.argv[2]
    with open(src, encoding='utf-8') as f:
        raw = f.read()
    with open(dst, 'w', encoding='utf-8') as f:
        f.write(normalize(raw))
    print(f'ste-normalized: {src} -> {dst}')
