"""paginacao.py -- arrumacao da paginacao do contrato gerado.

Copia de ~/.claude/skills/fazer-contrato/paginacao.py (skill "fazer-contrato" do
Mac principal, historico 02-04/09/2026), copiada para o leitor-mini em
2026-09-24. NAO alterada no essencial -- a unica diferenca desta copia e o
valor de WORD_TMP (ver abaixo), porque o leitor-mini usa uma subpasta propria
dentro do contentor do Word para nao colidir com a da skill "fazer-contrato"
quando ambas correm no mesmo Mac. Toda a logica de paginacao (estatica e
dinamica via Word/AppleScript) e identica ao original.

Se a skill "fazer-contrato" evoluir esta logica, replicar aqui manualmente
(nao ha symlink nem import cruzado entre os dois repositorios).

Parte estatica (sem Word): titulos de clausula com "manter com o seguinte", quebra de pagina
real antes do ANEXO I em vez dos ~50 paragrafos vazios do modelo.

Parte dinamica (precisa do Microsoft Word no Mac): pergunta ao Word em que pagina cai cada
paragrafo e apaga os paragrafos vazios que ficam no topo de uma pagina (a partir da 2.a),
repetindo ate estabilizar. So fecha o documento que abriu; nunca "close every document".
"""
import os
import re
import subprocess
import zipfile

PAR_RE = re.compile(r'<w:p\b[^>]*>.*?</w:p>', re.S)
TITULO_RE = re.compile(r'^\s*(Cláusula|CLÁUSULA|ANEXO)\b')


def _texto(p):
    return ''.join(re.findall(r'<w:t[^>]*>([^<]*)</w:t>', p))


def _com_ppr(p, tag):
    """Insere <tag> no pPr do paragrafo (depois do pStyle, se existir), sem duplicar."""
    if tag in p:
        return p
    if '<w:pPr>' in p:
        if '<w:pStyle' in p:
            return re.sub(r'(<w:pStyle\b[^>]*/>)', r'\1' + tag, p, count=1)
        return p.replace('<w:pPr>', '<w:pPr>' + tag, 1)
    return re.sub(r'(<w:p\b[^>]*>)', r'\1<w:pPr>' + tag + '</w:pPr>', p, count=1)


def _sem_pagebreak_vazio(p):
    return (_texto(p).strip() == '' and 'w:type="page"' not in p and '<w:sectPr' not in p
            and '<w:drawing' not in p and 'pageBreakBefore' not in p)


def arrumar_estatico(xml, modo_anexo='quebra'):
    """Devolve (xml_novo, log). modo_anexo: 'quebra' = quebra de pagina simples antes do ANEXO I (a pagina
    em branco para pagina impar e decidida depois com o Word); 'seccao_impar' = seccao a comecar em pagina
    impar (recurso sem Word: imprime bem, mas o Word nao mostra a pagina em branco no ecra)."""
    log = []
    ps = PAR_RE.findall(xml)
    novos = list(ps)
    # 1. titulos de clausula: keepNext no titulo, no subtitulo entre parenteses e nos vazios ate ao corpo
    n_keep = 0
    for i, p in enumerate(ps):
        t = _texto(p).strip()
        if not TITULO_RE.match(t):
            continue
        j = i
        while j < len(ps):
            tj = _texto(ps[j]).strip()
            if j > i and tj and not tj.startswith('('):
                break                      # primeiro paragrafo do corpo: fica sem keepNext
            novos[j] = _com_ppr(novos[j], '<w:keepNext/>')
            n_keep += 1
            j += 1
    log.append('keepNext em %d paragrafos de titulo/subtitulo' % n_keep)
    # 1a. exatamente UM paragrafo vazio entre o fim da ultima clausula e "elaborado em duplicado"
    #     (o modelo de termo certo nao tem nenhum, o SPR tem tres; pedido do utilizador 03/09)
    dup_i = [i for i, p in enumerate(ps) if 'elaborado em duplicado' in _texto(p)]
    if dup_i:
        d = dup_i[0]
        k = d - 1
        vazios = 0
        while k >= 0 and _sem_pagebreak_vazio(ps[k]) and novos[k] is not None:
            vazios += 1
            k -= 1
        if vazios == 0:
            vazio = re.sub(r'<w:r\b[^>]*>.*?</w:r>', '', novos[d], flags=re.S)
            vazio = vazio.replace('<w:keepNext/>', '')
            novos[d] = vazio + novos[d]      # o vazio novo entra antes do paragrafo do duplicado
            log.append('inserido 1 paragrafo vazio antes de "elaborado em duplicado"')
        elif vazios > 1:
            for j in range(k + 1, d - 1):
                novos[j] = None
            log.append('reduzidos %d paragrafos vazios para 1 antes de "elaborado em duplicado"' % vazios)
    # 1b. data + assinaturas ("Santo Tirso, ..." ate "O SEGUNDO OUTORGANTE") ficam na mesma pagina
    ini = [i for i, p in enumerate(ps) if _texto(p).strip().startswith('Santo Tirso, ')]
    if ini:
        i = ini[0]
        # o paragrafo "elaborado em duplicado" (poucos antes da data) vai junto com a data e as assinaturas
        dup = [j for j in range(max(0, i - 6), i) if 'elaborado em duplicado' in _texto(ps[j])]
        if dup:
            i = dup[0]
        fim = next((j for j in range(i, min(i + 20, len(ps))) if 'SEGUNDO OUTORGANTE' in _texto(ps[j])), None)
        if fim:
            for j in range(i, fim):
                novos[j] = _com_ppr(novos[j], '<w:keepNext/>')
            log.append('bloco duplicado+data+assinaturas mantido junto (%d paragrafos)' % (fim - i + 1))
    # 2. ANEXO I: comeca SEMPRE numa pagina impar (impressao frente e verso: o anexo vai numa folha
    #    sozinha). Quebra de seccao "oddPage": o Word insere a pagina em branco so quando e preciso.
    anexo = [i for i, p in enumerate(ps) if _texto(p).strip().startswith('ANEXO I')]
    m_sect = list(re.finditer(r'<w:sectPr\b[^>]*>.*?</w:sectPr>', xml, re.S))
    if anexo and m_sect:
        a = anexo[0]
        k = a - 1
        removidos = 0
        while k >= 0 and _sem_pagebreak_vazio(ps[k]):
            novos[k] = None
            removidos += 1
            k -= 1
        if modo_anexo == 'quebra':
            novos[a] = _com_ppr(novos[a].replace('<w:keepNext/>', ''), '<w:pageBreakBefore/>')
            log.append('ANEXO I: quebra de pagina; %d paragrafos vazios removidos antes dele' % removidos)
            anexo = []   # sem seccoes
        body_sect = m_sect[-1].group(0) if anexo else ''
        sect1 = re.sub(r'<w:type\b[^>]*/>', '', body_sect)
        sect1 = re.sub(r'<w:sectPr\b[^>]*>', '<w:sectPr>', sect1, count=1)
        # ultimo paragrafo antes do anexo fecha a 1.a seccao
        ult = novos[k] if anexo else ''
        if anexo:
            if '<w:pPr>' in ult:
                ult = ult.replace('</w:pPr>', sect1 + '</w:pPr>', 1)
            else:
                ult = re.sub(r'(<w:p\b[^>]*>)', r'\1<w:pPr>' + sect1.replace('\\', '\\\\') + '</w:pPr>', ult, count=1)
            novos[k] = ult
            novos[a] = novos[a].replace('<w:keepNext/>', '').replace('<w:pageBreakBefore/>', '')
            log.append('ANEXO I: seccao nova a comecar em pagina impar; %d paragrafos vazios removidos antes dele' % removidos)
    # reconstruir
    out = []
    pos = 0
    idx = 0
    for m in PAR_RE.finditer(xml):
        out.append(xml[pos:m.start()])
        if novos[idx] is not None:
            out.append(novos[idx])
        idx += 1
        pos = m.end()
    out.append(xml[pos:])
    xml_novo = ''.join(out)
    if anexo and m_sect:
        m_last = list(re.finditer(r'<w:sectPr\b[^>]*>.*?</w:sectPr>', xml_novo, re.S))[-1]
        sect = m_last.group(0)
        if '<w:type' in sect:
            sect2 = re.sub(r'<w:type\b[^>]*/>', '<w:type w:val="oddPage"/>', sect)
        elif '<w:pgSz' in sect:
            sect2 = sect.replace('<w:pgSz', '<w:type w:val="oddPage"/><w:pgSz', 1)
        else:
            sect2 = sect.replace('</w:sectPr>', '<w:type w:val="oddPage"/></w:sectPr>')
        xml_novo = xml_novo[:m_last.start()] + sect2 + xml_novo[m_last.end():]
    return xml_novo, log


# ----------------------------------------------------------------------------- Word
APPLESCRIPT = '''
on run argv
  set inPath to (POSIX file (item 1 of argv)) as text
  set out to ""
  tell application "Microsoft Word"
    launch
  end tell
  delay 3
  tell application "Microsoft Word"
    open file name inPath add to recent files no
    set d to document (item 2 of argv)
    tell d
      set n to count of paragraphs
      repeat with i from 1 to n
        set r to text object of paragraph i
        set t to content of r
        set s0 to start of content of r
        set r0 to create range d start s0 end s0
        set pg to (get range information r0 information type active end adjusted page number)
        if (length of t) > 30 then set t to text 1 thru 30 of t
        set out to out & i & tab & pg & tab & t & linefeed
      end repeat
    end tell
    close d saving no
  end tell
  return out
end run
'''


APPLESCRIPT_PDF = '''
on run argv
  set inPath to (POSIX file (item 1 of argv)) as text
  set outPath to (POSIX file (item 3 of argv)) as text
  tell application "Microsoft Word"
    launch
  end tell
  delay 2
  tell application "Microsoft Word"
    open file name inPath add to recent files no
    set d to document (item 2 of argv)
    save as d file name outPath file format format PDF
    close d saving no
  end tell
  return "ok"
end run
'''


def word_disponivel():
    return os.path.isdir('/Applications/Microsoft Word.app')


# Dentro do contentor da sandbox do Word: e a unica pasta onde o Word abre ficheiros por AppleScript sem
# pedir autorizacao ("permitir acesso ao documento") de cada vez. /private/tmp e pastas ocultas nem abrem;
# ~/Documents abre mas pergunta sempre (pedido do utilizador 03/09).
# NOTA (leitor-mini, 2026-09-24): subpasta propria "leitor-mini" -- distinta da "fazer-contrato" usada
# pela skill no Mac principal -- para os dois nunca colidirem quando correm ao mesmo tempo.
WORD_TMP = os.path.expanduser('~/Library/Containers/com.microsoft.Word/Data/tmp/leitor-mini')


def mapa_paginas(docx_path):
    """[(indice_1based, pagina, texto_curto)] via Word. Lanca RuntimeError se falhar."""
    os.makedirs(WORD_TMP, mode=0o700, exist_ok=True)
    nome = 'paginacao-%d.docx' % os.getpid()     # unico por processo: duas corridas em paralelo nao se pisam
    tmp = os.path.join(WORD_TMP, nome)
    with open(docx_path, 'rb') as fi, open(tmp, 'wb') as fo:
        fo.write(fi.read())
    try:
        import time
        for tentativa in range(3):
            r = subprocess.run(['osascript', '-', tmp, nome], input=APPLESCRIPT.encode(), capture_output=True, timeout=400)
            if r.returncode == 0:
                break
            time.sleep(6)   # o Word pode estar ainda a arrancar (erros -1708/-1712 na primeira chamada)
    finally:
        for f in (nome, '~$' + nome[2:]):
            try:
                os.remove(os.path.join(WORD_TMP, f))
            except OSError:
                pass
    if r.returncode != 0:
        raise RuntimeError('Word/osascript: ' + r.stderr.decode('utf-8', 'replace').strip()[:300])
    rows = []
    for ln in r.stdout.decode('utf-8', 'replace').split('\n'):
        p = ln.split('\t')
        if len(p) >= 2 and p[0].isdigit():
            rows.append((int(p[0]), int(p[1]), p[2] if len(p) > 2 else ''))
    if not rows:
        raise RuntimeError('Word devolveu um mapa vazio')
    return rows


def exportar_pdf(docx_path, pdf_path):
    """Exporta o docx para PDF pelo Word (save as ... format PDF). O docx e copiado para WORD_TMP (unica
    pasta que o Word abre por AppleScript sem pedir autorizacao; em /private/tmp dava -1708) e o PDF e
    gravado la e movido para pdf_path. Devolve None se correu bem, senao a mensagem de erro (nunca lanca)."""
    import shutil
    import time
    os.makedirs(WORD_TMP, mode=0o700, exist_ok=True)
    nome = 'pdf-%d.docx' % os.getpid()
    tmp = os.path.join(WORD_TMP, nome)
    tmp_pdf = os.path.join(WORD_TMP, 'pdf-%d.pdf' % os.getpid())
    shutil.copyfile(docx_path, tmp)
    erro = None
    try:
        for tentativa in range(3):
            r = subprocess.run(['osascript', '-', tmp, nome, tmp_pdf], input=APPLESCRIPT_PDF.encode(),
                               capture_output=True, timeout=400)
            if r.returncode == 0 and os.path.isfile(tmp_pdf) and os.path.getsize(tmp_pdf) > 0:
                break
            erro = r.stderr.decode('utf-8', 'replace').strip()[:300] or 'PDF vazio'
            time.sleep(6)
        else:
            return 'Word/osascript (PDF): ' + (erro or 'sem PDF')
        shutil.move(tmp_pdf, pdf_path)
        return None
    except Exception as e:  # noqa: BLE001
        return 'exportar_pdf: %s' % e
    finally:
        for f in (tmp, tmp_pdf, os.path.join(WORD_TMP, '~$' + nome[2:])):
            try:
                os.remove(f)
            except OSError:
                pass


def resumo_paginas(rows):
    pages = {}
    for i, pg, t in rows:
        pages.setdefault(pg, []).append((i, t))
    linhas = ['paginas: %d' % max(pages)]
    for pg in sorted(pages):
        l = pages[pg]
        topo = 0
        for _, t in l:
            if t.strip():
                break
            topo += 1
        primeiro = next((t for _, t in l if t.strip()), '(pagina vazia)')
        linhas.append('  pag %2d: %2d par, vazios no topo %d, comeca em %r' % (pg, len(l), topo, primeiro[:30]))
    return '\n'.join(linhas)


def vazios_no_topo(rows):
    """Indices (1-based, ordem do Word) dos paragrafos vazios no topo das paginas >= 2."""
    pages = {}
    for i, pg, t in rows:
        pages.setdefault(pg, []).append((i, t))
    alvo = []
    for pg, l in pages.items():
        if pg < 2:
            continue
        for i, t in l:
            if t.strip():
                break
            alvo.append(i)
    return alvo


def titulos_orfaos(rows):
    """Titulo genuinamente orfao: nem o proximo paragrafo com conteudo (tipicamente o subtitulo entre
    parenteses, garantido por keepNext) fica na mesma pagina. Um corpo/lista longa que continua na pagina
    seguinte, com o titulo+subtitulo ja fixados na pagina anterior, NAO e um problema (acontece noutras
    clausulas do proprio modelo, ex. Clausula Nona) e nao se assinala."""
    out = []
    for k, (i, pg, t) in enumerate(rows):
        if TITULO_RE.match(t.strip()):
            for j in range(k + 1, len(rows)):
                if rows[j][2].strip():
                    if rows[j][1] != pg:
                        out.append('%r (pag %d) sem nada a seguir na mesma pagina (proximo: pag %d)' % (t.strip(), pg, rows[j][1]))
                    break
    return out


def _remover_paragrafos(docx_path, indices_1based):
    """Remove paragrafos do word/document.xml pelos indices do Word (1-based, so se estiverem vazios)."""
    z = zipfile.ZipFile(docx_path)
    xml = z.read('word/document.xml').decode('utf-8')
    ps = PAR_RE.findall(xml)
    # O Word conta os paragrafos do corpo pela mesma ordem do XML (sem tabelas/caixas de texto neste modelo).
    alvo = set(indices_1based)
    out, pos, idx, removidos = [], 0, 0, 0
    for m in PAR_RE.finditer(xml):
        out.append(xml[pos:m.start()])
        idx += 1
        if idx in alvo and _sem_pagebreak_vazio(m.group(0)):
            removidos += 1
        else:
            out.append(m.group(0))
        pos = m.end()
    out.append(xml[pos:])
    novo = ''.join(out)
    tmp = docx_path + '.tmp'
    with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as zo:
        for item in z.infolist():
            data = z.read(item.filename)
            if item.filename == 'word/document.xml':
                data = novo.encode('utf-8')
            zo.writestr(item, data)
    z.close()
    os.replace(tmp, docx_path)
    return removidos


def _pagina_branca_explicita(docx_path):
    """Insere um paragrafo vazio com quebra de pagina antes do ANEXO I: a pagina em branco existe de facto."""
    z = zipfile.ZipFile(docx_path)
    xml = z.read('word/document.xml').decode('utf-8')
    m = None
    for mm in PAR_RE.finditer(xml):
        if _texto(mm.group(0)).strip().startswith('ANEXO I'):
            m = mm
            break
    if not m:
        return False
    branco = '<w:p><w:pPr><w:pageBreakBefore/></w:pPr></w:p>'
    xml = xml[:m.start()] + branco + xml[m.start():]
    tmp = docx_path + '.tmp'
    with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as zo:
        for item in z.infolist():
            data = z.read(item.filename)
            if item.filename == 'word/document.xml':
                data = xml.encode('utf-8')
            zo.writestr(item, data)
    z.close()
    os.replace(tmp, docx_path)
    return True


def arrumar_com_word(docx_path, max_iter=3):
    """Apaga vazios no topo das paginas ate estabilizar e torna explicita a pagina em branco antes do
    ANEXO I quando o contrato acaba em pagina impar. Devolve (log, resumo_final, orfaos)."""
    log = []
    rows = mapa_paginas(docx_path)
    for it in range(max_iter):
        alvo = vazios_no_topo(rows)
        if not alvo:
            break
        n = _remover_paragrafos(docx_path, alvo)
        log.append('iteracao %d: removidos %d paragrafos vazios no topo de pagina' % (it + 1, n))
        if n == 0:
            break
        rows = mapa_paginas(docx_path)
    anexo = [pg for _, pg, t in rows if t.strip().startswith('ANEXO I')]
    if anexo and anexo[0] % 2 == 0:
        # frente e verso: o anexo tem de comecar em pagina impar (folha propria)
        if _pagina_branca_explicita(docx_path):
            log.append('ANEXO I calhava na pagina %d (par): inserida pagina em branco antes dele' % anexo[0])
            rows = mapa_paginas(docx_path)
    anexo = [pg for _, pg, t in rows if t.strip().startswith('ANEXO I')]
    if anexo and anexo[0] % 2 == 0:
        log.append('AVISO: ANEXO I continua em pagina par (%d)' % anexo[0])
    return log, resumo_paginas(rows), titulos_orfaos(rows)
