"""plano de contas, centros de custo e ambientes em arvore (codigo hierarquico no padrao APP32)

Revision ID: a7c3e9d15b42
Revises: f4a7d2c8e615
Create Date: 2026-10-27 18:00:00.000000

Preserva os ids (gastos, despesas, maquinas, roteiros continuam ligados). O codigo antigo vira `chave`
(estavel); o `codigo` passa a ser o da arvore. Plano: tabela fixa antigo -> novo; conta fora da tabela vira
filha do pai com o proximo sufixo. Centros e Ambientes existentes viram raizes analiticas 1, 2, 3...
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'a7c3e9d15b42'
down_revision: Union[str, None] = 'f4a7d2c8e615'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

# chave antiga -> codigo novo (copia congelada do plano padrao desta versao)
MAPA = {
    "PC-RECEITAS": "1", "PC-RECEITA-VENDAS": "1.1",
    "PC-CUSTOS": "2", "PC-DESPESAS": "3", "PC-IMPOSTOS": "4",
    "PC-CUSTOS-PESSOAS": "2.1", "PC-CUSTOS-MAQEQUIP": "2.2", "PC-CUSTOS-INSTAL": "2.3",
    "PC-CUSTOS-INSUMOS": "2.4", "PC-CUSTOS-OUTROS": "2.5",
    "PC-DESP-COMERCIAIS": "3.1", "PC-DESP-FINANCEIRAS": "3.2", "PC-DESP-TRIBUTARIAS": "3.3", "PC-DESP-OUTRAS": "3.4",
    "PC-IMP-VENDAS": "4.1", "PC-IMP-IRCSLL": "4.2",
    "PC-IMP-IPI": "4.1.01", "PC-IMP-ICMSST": "4.1.02", "PC-IMP-ICMS": "4.1.03", "PC-IMP-PIS": "4.1.04", "PC-IMP-COFINS": "4.1.05",
    "PC-PESSOAS-SALARIOS": "2.1.01", "PC-PESSOAS-OUTREM": "2.1.02", "PC-PESSOAS-ALIM": "2.1.03", "PC-PESSOAS-TRANSP": "2.1.04",
    "PC-PESSOAS-ASSMED": "2.1.05", "PC-PESSOAS-FGTS": "2.1.06", "PC-PESSOAS-INSS": "2.1.07", "PC-PESSOAS-PROVIS": "2.1.08",
    "PC-PESSOAS-VTVA": "2.1.09",
    "PC-MAQEQ-DEPREC": "2.2.01", "PC-MAQEQ-MANUT": "2.2.02", "PC-MAQEQ-ENERGIA": "2.2.03", "PC-MAQEQ-SEGURO": "2.2.04", "PC-MAQEQ-OUTROS": "2.2.05",
    "PC-INSTAL-ALUGUEL": "2.3.01", "PC-INSTAL-ENERGIA": "2.3.02", "PC-INSTAL-MANUT": "2.3.03", "PC-INSTAL-AGUA": "2.3.04",
    "PC-INSTAL-IPTU": "2.3.05", "PC-INSTAL-LIMPSEG": "2.3.06", "PC-INSTAL-DEPREC": "2.3.07",
}
LARGURAS = (1, 1, 2, 3)


def _largura(nivel: int) -> int:
    return LARGURAS[nivel - 1] if nivel <= len(LARGURAS) else LARGURAS[-1]


def _colunas_de_no(tabela: str) -> None:
    op.add_column(tabela, sa.Column('sufixo', sa.String(length=10), nullable=True))
    op.add_column(tabela, sa.Column('aceita_lancamento', sa.Boolean(), server_default=sa.true(), nullable=False))
    op.add_column(tabela, sa.Column('ativo', sa.Boolean(), server_default=sa.true(), nullable=False))
    op.add_column(tabela, sa.Column('chave', sa.String(length=40), nullable=True))
    op.create_unique_constraint(f'uq_{tabela}_chave', tabela, ['chave'])


def _renumerar_planos(bind) -> None:
    bind.execute(sa.text("update plano_de_contas set chave = codigo where chave is null"))
    linhas = bind.execute(sa.text("select id, chave, parent_id, tipo from plano_de_contas order by id")).all()
    por_id = {r.id: r for r in linhas}
    filhos: dict[int | None, list] = {}
    for r in linhas:
        filhos.setdefault(r.parent_id, []).append(r)

    # a raiz/grupo de Receitas entra se faltar (a DRE usa o tipo 'receita')
    chaves = {r.chave for r in linhas}
    novos_ids: dict[str, int] = {}
    if linhas and "PC-RECEITAS" not in chaves:  # banco novo (vazio): quem semeia é o start-up
        novos_ids["PC-RECEITAS"] = bind.execute(sa.text(
            "insert into plano_de_contas (codigo, nome, tipo, parent_id, sufixo, aceita_lancamento, ativo, chave) "
            "values ('TMP-R', 'Receitas', 'receita', null, '1', false, true, 'PC-RECEITAS') returning id")).scalar_one()
        novos_ids["PC-RECEITA-VENDAS"] = bind.execute(sa.text(
            "insert into plano_de_contas (codigo, nome, tipo, parent_id, sufixo, aceita_lancamento, ativo, chave) "
            "values ('TMP-V', 'Receita de Vendas', 'receita', :pai, '1', true, true, 'PC-RECEITA-VENDAS') returning id"),
            {"pai": novos_ids["PC-RECEITAS"]}).scalar_one()

    final: dict[int, str] = {}
    usados = set(MAPA.values())
    for r in linhas:  # mapeadas
        if r.chave in MAPA:
            final[r.id] = MAPA[r.chave]
    for chave, id_ in novos_ids.items():
        final[id_] = MAPA[chave]

    def profundidade(r) -> int:
        n, atual = 0, r
        while atual.parent_id is not None and atual.parent_id in por_id:
            n, atual = n + 1, por_id[atual.parent_id]
        return n

    for r in sorted((x for x in linhas if x.chave not in MAPA), key=lambda x: (profundidade(x), x.id)):
        pai_codigo = final.get(r.parent_id) if r.parent_id is not None else None
        nivel = (pai_codigo.count(".") + 2) if pai_codigo else 1
        n = 1
        while True:
            sufixo = str(n).zfill(_largura(nivel))
            codigo = f"{pai_codigo}.{sufixo}" if pai_codigo else sufixo
            if codigo not in usados:
                break
            n += 1
        usados.add(codigo)
        final[r.id] = codigo

    com_filhos = {r.parent_id for r in linhas if r.parent_id is not None}
    if "PC-RECEITAS" in novos_ids:
        com_filhos.add(novos_ids["PC-RECEITAS"])
    bind.execute(sa.text("update plano_de_contas set codigo = 'TMP-' || id"))
    for id_, codigo in final.items():
        bind.execute(sa.text(
            "update plano_de_contas set codigo = :c, sufixo = :s, aceita_lancamento = :a where id = :id"),
            {"c": codigo, "s": codigo.rsplit(".", 1)[-1], "a": id_ not in com_filhos, "id": id_})


def _renumerar_raizes(bind, tabela: str) -> None:
    bind.execute(sa.text(f"update {tabela} set chave = codigo where chave is null"))
    ids = [r.id for r in bind.execute(sa.text(f"select id from {tabela} order by id")).all()]
    bind.execute(sa.text(f"update {tabela} set codigo = 'TMP-' || id"))
    for n, id_ in enumerate(ids, start=1):
        bind.execute(sa.text(f"update {tabela} set codigo = :c, sufixo = :c where id = :id"), {"c": str(n), "id": id_})


def upgrade() -> None:
    # --- plano de contas
    _colunas_de_no('plano_de_contas')
    op.add_column('plano_de_contas', sa.Column('codigo_reduzido', sa.String(length=20), nullable=True))
    op.add_column('plano_de_contas', sa.Column('codigo_externo', sa.String(length=40), nullable=True))
    op.add_column('plano_de_contas', sa.Column('observacoes', sa.String(length=500), nullable=True))

    # --- centro de custo
    _colunas_de_no('centro_de_custo')
    op.add_column('centro_de_custo', sa.Column('parent_id', sa.Integer(), nullable=True))
    op.add_column('centro_de_custo', sa.Column('descricao', sa.String(length=500), nullable=True))
    op.add_column('centro_de_custo', sa.Column('gestor_id', sa.Integer(), nullable=True))
    op.create_foreign_key('fk_centro_de_custo_parent', 'centro_de_custo', 'centro_de_custo', ['parent_id'], ['id'])
    op.create_foreign_key('fk_centro_de_custo_gestor', 'centro_de_custo', 'funcionario', ['gestor_id'], ['id'])

    # --- ambiente
    _colunas_de_no('ambiente')
    op.add_column('ambiente', sa.Column('parent_id', sa.Integer(), nullable=True))
    op.create_foreign_key('fk_ambiente_parent', 'ambiente', 'ambiente', ['parent_id'], ['id'])

    bind = op.get_bind()
    _renumerar_planos(bind)
    _renumerar_raizes(bind, 'centro_de_custo')
    _renumerar_raizes(bind, 'ambiente')


def downgrade() -> None:
    bind = op.get_bind()
    for tabela in ('plano_de_contas', 'centro_de_custo', 'ambiente'):
        # volta ao codigo antigo (so quando a chave ainda existe); fase 1 libera os codigos
        bind.execute(sa.text(f"update {tabela} set codigo = 'TMP-' || id"))
        bind.execute(sa.text(f"update {tabela} set codigo = chave where chave is not null"))
        bind.execute(sa.text(f"update {tabela} set codigo = 'NOVO-' || id where codigo like 'TMP-%'"))

    op.drop_constraint('fk_ambiente_parent', 'ambiente', type_='foreignkey')
    op.drop_column('ambiente', 'parent_id')
    op.drop_constraint('fk_centro_de_custo_gestor', 'centro_de_custo', type_='foreignkey')
    op.drop_constraint('fk_centro_de_custo_parent', 'centro_de_custo', type_='foreignkey')
    op.drop_column('centro_de_custo', 'gestor_id')
    op.drop_column('centro_de_custo', 'descricao')
    op.drop_column('centro_de_custo', 'parent_id')
    op.drop_column('plano_de_contas', 'observacoes')
    op.drop_column('plano_de_contas', 'codigo_externo')
    op.drop_column('plano_de_contas', 'codigo_reduzido')
    for tabela in ('ambiente', 'centro_de_custo', 'plano_de_contas'):
        op.drop_constraint(f'uq_{tabela}_chave', tabela, type_='unique')
        for coluna in ('chave', 'ativo', 'aceita_lancamento', 'sufixo'):
            op.drop_column(tabela, coluna)
