#!/usr/bin/env python3
"""Gera o README.md (indice) do conjunto de dossies.

Idempotente: le os 00-RESUMO.md de cada modulo, cruza com o registro de
modulos declarado em `_protocolo/projetos.json` e escreve o indice.

Configuracao em `_protocolo/projetos.json` (alias, extras, titulo, descricao).

Uso:
  python3 dossie_modules/_tools/gerar-readme.py --projeto <id>
"""
import argparse
import datetime
import glob
import json
import os
import re
import subprocess
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import _cfg  # noqa: E402

CANON = None          # preenchido em runtime a partir da configuracao


def sh(cmd, cwd=None):
    return subprocess.run(cmd, shell=True, capture_output=True, text=True,
                          cwd=cwd).stdout


def registro(cfg):
    """Itens do registro oficial, ja com o alias aplicado.

    No modelo `pastas` o inventario da F1 e o registro: ele lista as pastas de
    modulo (com profundidade configuravel) e o slug vira `grupo/modulo`.
    """
    mod = cfg['modulos']
    alias = cfg.get('alias') or {}
    if mod.get('modelo') == 'pastas':
        inv = os.path.join(cfg['dos_dir'], 'inventario.json')
        if not os.path.exists(inv):
            return []
        data = json.load(open(inv, encoding='utf-8'))
        out = []
        for it in (data.get('modulos_meta') or {}).get('itens') or []:
            slug = it.get('slug') or it.get('nome', '')
            out.append({'id': '', 'nome': it.get('nome', slug), 'slug': slug,
                        'status': '', 'pasta': alias.get(slug, slug)})
        return out
    if mod.get('modelo') != 'registry' or not mod.get('comando'):
        return []
    raw = sh(mod['comando'], cwd=cfg['raiz_workspace'])
    sep = mod.get('separador', '|')
    campos = mod.get('campos') or ['id', 'nome', 'slug', 'status']
    out = []
    for l in raw.splitlines():
        if not l.strip():
            continue
        partes = [x.strip() for x in (l.split(sep) if sep else [l])]
        if len(partes) < len(campos):
            continue
        reg = dict(zip(campos, partes))
        slug = reg.get('slug') or reg.get('nome')
        out.append({'id': reg.get('id', ''), 'nome': reg.get('nome', ''),
                    'slug': slug, 'status': reg.get('status', ''),
                    'pasta': alias.get(slug, slug)})
    return out


def meta(dos, folder, canon):
    r = os.path.join(dos, folder, '00-RESUMO.md')
    if not os.path.exists(r):
        return None
    s = open(r, encoding='utf-8', errors='ignore').read()
    out = {'folder': folder}
    titulo = s.splitlines()[0].lstrip('# ').strip() if s.strip() else folder
    out['titulo'] = re.sub(r'\s*[-—|]\s*(Resumo Executivo|Resumo).*$', '',
                           titulo).strip()
    m = re.search(r'\*\*Estado:?\*\*\s*(.+)', s)
    out['estado'] = m.group(1).strip() if m else ''
    m = re.search(r'\*\*Natureza:?\*\*\s*(.+)', s)
    out['natureza'] = m.group(1).strip() if m else ''
    m = re.search(r'##+\s*\d*\.?\s*(Objetivo|Propósito)\s*\n+(.+?)(?=\n##|\n---|\Z)',
                  s, re.S)
    out['objetivo'] = ' '.join(m.group(2).split()) if m else ''
    files = sorted(glob.glob(os.path.join(dos, folder, '*.md')))
    out['nfiles'] = len(files)
    out['nlines'] = sum(len(open(f, encoding='utf-8', errors='ignore')
                            .read().splitlines()) for f in files)
    out['extras'] = [os.path.basename(f) for f in files
                     if os.path.basename(f) not in canon]
    return out


def linha_tabela(m, link_prefix=''):
    titulo = (m['titulo'] or '').replace('|', '/')
    obj = m.get('objetivo') or ''
    sent = re.split(r'(?<=[.!?])\s', obj)[0] if obj else ''
    if len(sent) > 150:
        sent = sent[:147].rsplit(' ', 1)[0] + '…'
    sent = sent.replace('|', '/')
    extra = f' (+{len(m["extras"])})' if m.get('extras') else ''
    pasta = m['folder']
    return (f'| [`{pasta}/`]({link_prefix}{pasta}/00-RESUMO.md) | {titulo} | '
            f'{sent} | {m["nfiles"]}{extra} |')


def contar(entries, M):
    nf = nl = 0
    for pasta, _ in entries:
        m = M.get(pasta)
        if m:
            nf += m['nfiles']
            nl += m['nlines']
    return nf, nl


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    _cfg.cli_projeto(ap)
    ap.add_argument('--forcar', action='store_true',
                    help='permite sobrescrever arquivo que nao e gerado')
    args = ap.parse_args()

    cfg = _cfg.load(args.projeto, args.raiz)
    dos = cfg['dos_dir']
    if not os.path.isdir(dos):
        _cfg.die(f"diretorio de dossie inexistente: {dos}")
    canon = cfg['arquivos_canonicos']

    # pastas de dossie de OUTROS projetos ficam fora do escopo
    estrangeiras = set()
    for p in (_cfg.carregar_config().get('projetos') or {}).values():
        d = os.path.abspath(os.path.join(cfg['raiz_workspace'], p.get('dos', '')))
        if d != os.path.abspath(dos):
            estrangeiras.add(d)

    M = {}
    folders = sorted(d for d in os.listdir(dos)
                     if os.path.isdir(os.path.join(dos, d))
                     and not d.startswith('_')
                     and os.path.abspath(os.path.join(dos, d)) not in estrangeiras)
    for f in folders:
        m = meta(dos, f, canon)
        if m:
            M[f] = m

    reg = registro(cfg)
    v6_entries, outros = [], []
    vistos = set()
    grupo_extras_pre = []
    for chave, grupo in (cfg.get('extras') or {}).items():
        grupo_extras_pre.append((
            grupo.get('rotulo', chave),
            grupo.get('descricao', ''),
            [(s, {}) for s in (grupo.get('modulos') or {})]))
    for r in reg:
        p = r['pasta']
        if p in M and p not in vistos:
            v6_entries.append((p, r))
            vistos.add(p)
    slugs_extras = {s for _, _, itens in grupo_extras_pre for s, _ in itens}
    for f in folders:
        if f not in vistos and f not in slugs_extras:
            outros.append((f, {}))
    sem_dossie = [r for r in reg if r['pasta'] not in M]
    tab_sem = ''
    if sem_dossie:
        linhas_sem = '\n'.join(
            f"| `{r['slug']}` | {r['nome']} | `{r['pasta']}` |" for r in sem_dossie)
        tab_sem = ('| Slug | Nome | Pasta esperada |\n|---|---|---|\n' + linhas_sem)

    extras = cfg.get('extras') or {}
    grupo_extras = []
    for chave, grupo in extras.items():
        itens = [(s, {}) for s in (grupo.get('modulos') or {}) if s in M]
        grupo_extras.append((grupo.get('rotulo', chave),
                             grupo.get('descricao', ''), itens))

    n6, l6 = contar(v6_entries, M)
    nout, lout = contar(outros, M)
    panorama = cfg.get('panorama', '_00_panorama')
    pdir = os.path.join(dos, panorama)
    pfiles = sorted(glob.glob(os.path.join(pdir, '*.md'))) if os.path.isdir(pdir) else []
    npa = len(pfiles)
    lpa = sum(len(open(f, encoding='utf-8', errors='ignore').read().splitlines())
              for f in pfiles)

    link_prefix = ''
    if os.path.basename(dos) != 'dossie_modules':
        link_prefix = ''

    tabela_reg = ['| Pasta | Módulo | Objetivo | Arquivos |', '|---|---|---|---|']
    for pasta, r in v6_entries:
        tabela_reg.append(linha_tabela(M[pasta], link_prefix))
    tab_reg = '\n'.join(tabela_reg)

    blocos_extras = []
    for rotulo, desc, itens in grupo_extras:
        n, l = contar(itens, M)
        linhas = ['| Pasta | Módulo | Objetivo | Arquivos |', '|---|---|---|---|']
        for pasta, _ in itens:
            linhas.append(linha_tabela(M[pasta], link_prefix))
        blocos_extras.append(
            f"### {rotulo}\n\n{desc}\n\n" + '\n'.join(linhas) +
            f"\n\n**Subtotal: {len(itens)} módulos · {n} arquivos · {l} linhas**\n")

    tabela_outros = '\n'.join(
        f'- [`{f}/`]({f}/00-RESUMO.md) — {M[f]["titulo"]}' for f, _ in outros)

    protocolo_dir = os.path.join(os.path.dirname(dos), '_protocolo')
    tem_protocolo = os.path.isdir(protocolo_dir)
    nprot = len(glob.glob(os.path.join(protocolo_dir, '*.md'))) if tem_protocolo else 0
    lprot = (sum(len(open(f, encoding='utf-8', errors='ignore').read().splitlines())
                 for f in glob.glob(os.path.join(protocolo_dir, '*.md')))
             if tem_protocolo else 0)

    # prefixo relativo do dossie ate a raiz da base (onde vivem _protocolo e _template)
    raiz_base = os.path.dirname(dos)
    pref = os.path.relpath(raiz_base, dos)          # '' ou '..'
    pref = ('' if pref == '.' else pref + '/')

    # tabela "comece por aqui": so o que existe de fato
    itens = [
        (f'{panorama}/00-RESUMO.md',
         'Visão geral da plataforma, arquitetura, dívida técnica, inventário dos módulos'),
        (f'{panorama}/10-INVENTARIO-DE-MODULOS.md',
         '**Lista canônica** dos módulos do registro, com classificação e cobertura'),
        (f'{panorama}/11-INVENTARIO-MECANICO.md',
         'Volumetria, diff entre versões, rotas, views e tabelas (gerado)'),
        (f'{panorama}/17-REGISTRO-DE-BUGS.md',
         '**Registro master de todos os bugs**, por severidade e categoria'),
        (f'{panorama}/18-BACKLOG-PRIORIZADO.md',
         'Backlog P0–P3 com esforço, risco e sequenciamento em sprints'),
        (pref + '_protocolo/README.md',
         '**O método**: fases, princípios, gates e como replicar em outro projeto'),
        (pref + '_protocolo/04-DECISAO-MODULOS.md',
         'Como decidir o que é um módulo (3 modelos de registro + taxonomia)'),
        (pref + '_protocolo/07-RUNBOOK.md',
         'Passo a passo para aplicar o protocolo num projeto novo'),
        (pref + '_template/_CONTRATO.md',
         'Contrato de produção dos dossiês (estrutura, estilo, verificação)'),
    ]
    linhas_comece = []
    for rel, desc in itens:
        # rel e sempre relativo ao diretorio do dossie
        alvo = os.path.normpath(os.path.join(dos, rel))
        if os.path.exists(alvo):
            link = rel if not rel.startswith('../') else rel
            linhas_comece.append(f'| [`{rel.lstrip("./")}`]({link}) | {desc} |')
    comece = '\n'.join(linhas_comece)

    today = datetime.date.today().isoformat()
    total_pastas = len(folders) + (1 if os.path.isdir(pdir) else 0)
    total_arquivos = sum(m['nfiles'] for m in M.values()) + npa
    total_linhas = sum(m['nlines'] for m in M.values()) + lpa

    def link_panel(rel, rotulo, desc):
        """So referencia documentos que existem de fato."""
        if os.path.exists(os.path.join(dos, rel)):
            return f"| [`{rel}`]({rel}) | {desc} |"
        return ''

    doc = f"""# Dossiê de Módulos — {cfg['titulo']}

> **Objetivo:** especificação de referência para reconstruir o sistema em
> **PHP 8.x + Laravel 11/12**. Cada módulo tem uma pasta com
> {len(canon)} documentos cobrindo objetivo, efeito, comportamento, interação
> do usuário, particulares técnicas, estrutura de dados, migração, testes e gaps.
>
> **Gerado em:** {today} · **Projeto:** `{cfg['id']}` ·
> **Escopo:** {total_pastas} pastas ({len(reg)} módulos do registro,
> {len(v6_entries)} com dossiê + {sum(len(i) for _, _, i in grupo_extras)} extras + panorama) ·
> **{total_arquivos}** arquivos · **{total_linhas}** linhas

{cfg.get('descricao', '')}

---

## Comece por aqui

| Documento | O que resolve |
|---|---|
{comece}

---

## Convenções

Cada pasta de módulo segue a mesma estrutura de {len(canon)} arquivos:

| Arquivo | Conteúdo |
|---|---|
| `00-RESUMO.md` | Objetivo, efeito, comportamento, interação, particulares, dados, testes, migração, resumo |
| `01-ARQUITETURA.md` | Fluxo da requisição, classes envolvidas, diagrama, dependências |
| `02-MODELAGEM.md` | Tabelas, colunas, relacionamentos, views SQL, migrations, seeds |
| `03-ROTAS-E-CONTROLADORES.md` | Rotas (método, URI, nome, action, middleware), requests, resources |
| `04-VIEWS-E-UX.md` | Blades, layouts do portal, JS/Ajax, exportações, o que o usuário vê |
| `05-REGRAS-E-PORTABILIDADE.md` | Regras não óbvias, acoplamentos, armadilhas, dívida |
| `06-MIGRACAO-MODERNA.md` | Plano de reconstrução em Laravel 11/12 |
| `07-TESTES.md` | Estratégia, casos concretos, fixtures, como rodar |
| `08-GAPS-E-DIVIDAS.md` | Bugs, funcionalidades mortas, riscos, mapa entre versões |

Módulos densos recebem arquivos extras numerados (`09-`, `10-`…), sempre
listados no índice do `00-RESUMO.md` da pasta.

---

## Módulos do registro

{tab_reg}

**Subtotal:** {len(v6_entries)} módulos · {n6} arquivos · {l6} linhas

---

{("## Registro sem dossiê" + chr(10) + chr(10) + tab_sem + chr(10) + chr(10) + "---" + chr(10) + chr(10)) if sem_dossie else ""}## Grupos adicionais

{chr(10).join(blocos_extras) if blocos_extras else '_Nenhum._'}

---

## Panorama da plataforma

| Pasta | Arquivos | Linhas |
|---|---|---|
| `{panorama}/` | {npa} | {lpa} |

"""

    if outros:
        doc += f"## Outros dossiês\n\n{tabela_outros}\n\n"

    if tem_protocolo:
        doc += f"""---

## Protocolo

O método que produziu este dossiê está em `_protocolo/` ({nprot} documentos,
{lprot} linhas) e vale para qualquer projeto:

| Documento | Conteúdo |
|---|---|
| `README.md` | O método em uma página: as 7 fases, os princípios, as ferramentas |
| `01-PRINCIPIOS.md` | Os 10 princípios, com contraexemplo real |
| `02-FASES.md` | Fase a fase: entrada, ação, saída e gate |
| `03-CONTRATO.md` | O contrato de produção, genérico |
| `04-DECISAO-MODULOS.md` | Como decidir o que é um módulo |
| `05-CALIDADE.md` | Os gates automatizados e seus limiares |
| `06-LICOES.md` | As armadilhas observadas na execução real |
| `07-RUNBOOK.md` | Passo a passo para um projeto novo |

### Ferramentas

```bash
python3 dossie_modules/_tools/inventario.py    --projeto {cfg['id']}   # F1
python3 dossie_modules/_tools/scaffold.py      --projeto {cfg['id']} --slug <slug> --titulo "<Titulo>"
python3 dossie_modules/_tools/gerar-readme.py  --projeto {cfg['id']}   # F6
python3 dossie_modules/_tools/auditar.py       --projeto {cfg['id']}   # F6
```

"""

    out = os.path.join(dos, 'README.md')
    gerados = {os.path.join(cfg['raiz_workspace'], p)
               for p in (cfg.get('gerados') or [])}
    if os.path.exists(out) and out not in gerados and not args.forcar:
        _cfg.die(f"'{os.path.relpath(out, cfg['raiz_workspace'])}' existe e nao "
                 f"esta declarado como GERADO em projetos.json. Use --forcar.")

    # secao "outros projetos da base" — a base e' compartilhada
    try:
        todos = _cfg.carregar_config().get('projetos', {})
    except Exception:                                      # noqa: BLE001
        todos = {}
    outros_p = []
    for pid, p in sorted(todos.items()):
        if pid == cfg['id']:
            continue
        rel = p.get('dos', '')
        link = rel if rel.startswith('dossie_modules/') else rel
        pasta = os.path.join(cfg['raiz_workspace'], rel)
        nmod = 0
        if os.path.isdir(pasta):
            nmod = len([d for d in os.listdir(pasta)
                        if os.path.isdir(os.path.join(pasta, d))
                        and not d.startswith('_')])
        estado = p.get('estado', 'preparado')
        modelo = (p.get('modulos') or {}).get('modelo', '?')
        outros_p.append(
            f"| [`{pid}`]({link}/_00_panorama/10-INVENTARIO-DE-MODULOS.md) | "
            f"{p.get('titulo', pid)} | `{modelo}` | {nmod} | `{estado}` |")
    if outros_p:
        doc += (
            "---\n\n## Outros projetos da base\n\n"
            "O mesmo protocolo se aplica a qualquer projeto. Cada um tem seu "
            "próprio diretório de dossiê e seu perfil em "
            "`_protocolo/projetos.json`.\n\n"
            "| Projeto | Nome | Modelo de registro | Módulos | Estado |\n"
            "|---|---|---|---|---|\n" + '\n'.join(outros_p) + "\n\n")

    with open(out, 'w', encoding='utf-8') as fh:
        fh.write(doc)

    print(f'README gerado: {len(doc.splitlines())} linhas -> {out}')
    print(f'  registro: {len(v6_entries)} módulos / {n6} arquivos / {l6} linhas')
    for rotulo, _, itens in grupo_extras:
        n, l = contar(itens, M)
        print(f'  {rotulo}: {len(itens)} módulos / {n} arquivos / {l} linhas')
    print(f'  outros: {len(outros)} -> {[f for f, _ in outros]}')
    print(f'  panorama: {npa} arquivos / {lpa} linhas')
    return 0


if __name__ == '__main__':
    sys.exit(main())
