"""Cópia e exclusão de Planejamentos (um schema do Postgres por Planejamento — ver `planejamento_schema`).

Copiar = criar o schema novo com as mesmas tabelas (a partir dos models), copiar os dados mantendo os ids, acertar as
sequências e registrar o Planejamento. Tudo numa transação: se algo falhar, nada fica pela metade (o DDL do Postgres é
transacional).
"""

from datetime import datetime

from sqlalchemy import Engine, MetaData, Table, delete, insert, select, text, update
from sqlalchemy.engine import Connection

from custos_precificacao import planejamento_schema as ps
from custos_precificacao.models import Planejamento, PlanejamentoVersao, Premissa, VolumeProjetado


class ErroDeCopia(ValueError):
    """Pedido inválido (origem inexistente, código repetido, banco sem suporte)."""


def _q(identificador: str) -> str:
    return '"' + ps.validar_schema(identificador) + '"'


def _espelhar(comum: str, destino: str) -> tuple[MetaData, list[Table]]:
    """Cópia dos models com as tabelas do Planejamento no schema `destino` (as compartilhadas apontam para `comum`)."""
    from custos_precificacao.models import Base

    meta = MetaData()
    novas: list[Table] = []

    def referido(_tabela, para_schema, restricao, _antigo):
        return comum if restricao.referred_table.name in ps.COMPARTILHADAS else para_schema

    for tabela in Base.metadata.sorted_tables:
        compartilhada = tabela.name in ps.COMPARTILHADAS
        copia = tabela.to_metadata(meta, schema=comum if compartilhada else destino, referred_schema_fn=referido)
        if not compartilhada:
            novas.append(copia)
    return meta, novas


def _copiar_dados(conexao: Connection, origem: str, destino: str, tabelas: list[Table]) -> None:
    for tabela in tabelas:
        colunas = ", ".join(f'"{c.name}"' for c in tabela.columns)
        conexao.execute(text(f'INSERT INTO {_q(destino)}."{tabela.name}" ({colunas}) SELECT {colunas} FROM {_q(origem)}."{tabela.name}"'))


def _acertar_sequencias(conexao: Connection, destino: str, tabelas: list[Table]) -> None:
    for tabela in tabelas:
        if len(tabela.primary_key.columns) != 1:
            continue  # chave composta (tabelas de exceção): não há sequência
        coluna = next(iter(tabela.primary_key.columns))
        if coluna.foreign_keys or coluna.type.python_type is not int:
            continue
        alvo = f'{_q(destino)}."{tabela.name}"'
        conexao.execute(
            text(
                f'SELECT setval(pg_get_serial_sequence(:alvo, :coluna), COALESCE(MAX("{coluna.name}"), 1), '
                f'MAX("{coluna.name}") IS NOT NULL) FROM {alvo}'
            ),
            {"alvo": alvo, "coluna": coluna.name},
        )


def _copiar_versao_do_alembic(conexao: Connection, origem: str, destino: str) -> None:
    conexao.execute(text(f'CREATE TABLE {_q(destino)}.alembic_version (LIKE {_q(origem)}.alembic_version INCLUDING ALL)'))
    conexao.execute(text(f"INSERT INTO {_q(destino)}.alembic_version SELECT * FROM {_q(origem)}.alembic_version"))


def _clonar_schema(conexao: Connection, comum: str, origem: str, destino: str, *, manter_id: int, novo_id: int) -> None:
    """Cria o schema `destino` como cópia de `origem`. Volumes e Premissas pertencem a um Planejamento: na cópia ficam só
    os do Planejamento `manter_id`, agora do `novo_id`."""
    conexao.execute(text(f"CREATE SCHEMA {_q(destino)}"))
    meta, tabelas = _espelhar(comum, destino)
    meta.create_all(conexao, tables=tabelas)
    _copiar_dados(conexao, origem, destino, tabelas)
    for modelo in (VolumeProjetado, Premissa):
        tabela = meta.tables[f"{destino}.{modelo.__tablename__}"]
        conexao.execute(delete(tabela).where(tabela.c.planejamento_id != manter_id))
        conexao.execute(update(tabela).values(planejamento_id=novo_id))
    _acertar_sequencias(conexao, destino, tabelas)
    _copiar_versao_do_alembic(conexao, origem, destino)


def copiar_planejamento(
    engine: Engine,
    *,
    origem_codigo: str,
    codigo: str | None = None,
    nome: str | None = None,
    ano: int | None = None,
    criado_por: str | None = None,
) -> str:
    """Cria um Planejamento como cópia completa do `origem_codigo`. Sem `codigo`/`nome`, usa "P<ano>V<nn>" e
    "Planejamento <ano> Vrs. <nn>". Devolve o código criado."""
    if not ps.e_postgres(engine):
        raise ErroDeCopia("Copiar Planejamentos exige Postgres (um schema por Planejamento)")
    codigo = (codigo or "").strip()
    nome = (nome or "").strip()
    comum = ps.schema_comum(engine)
    registro = Planejamento.__table__

    with engine.begin() as conexao:
        origem = conexao.execute(select(registro).where(registro.c.codigo == origem_codigo)).mappings().first()
        if origem is None:
            raise ErroDeCopia(f"Planejamento de origem {origem_codigo!r} não encontrado")
        if not origem["schema"]:
            raise ErroDeCopia(f"Planejamento de origem {origem_codigo!r} ainda não tem os dados separados")
        ano = ano or origem["ano"]
        numero = None
        if ano:
            ultimo = conexao.execute(
                select(registro.c.numero_versao).where(registro.c.ano == ano).order_by(registro.c.numero_versao.desc()).limit(1)
            ).scalar()
            numero = (ultimo or 0) + 1
        if not codigo:
            if not ano:
                raise ErroDeCopia("Informe o código (a origem não tem ano para montar um)")
            codigo = f"P{ano}V{numero:02d}"
        if not nome:
            nome = f"Planejamento {ano} Vrs. {numero:02d}" if ano else codigo
        if len(codigo) > 20:
            raise ErroDeCopia("O código pode ter no máximo 20 caracteres")
        if conexao.execute(select(registro.c.id).where(registro.c.codigo == codigo)).first() is not None:
            raise ErroDeCopia(f"Já existe um Planejamento com código {codigo!r}")
        novo_id = conexao.execute(
            insert(registro)
            .values(
                codigo=codigo, nome=nome, ano=ano, numero_versao=numero, origem_id=origem["id"], oficial=False,
                criado_em=datetime.utcnow(), criado_por=criado_por,
            )
            .returning(registro.c.id)
        ).scalar_one()
        destino = ps.nome_do_schema(novo_id, comum)
        conexao.execute(update(registro).where(registro.c.id == novo_id).values(schema=destino))
        _clonar_schema(conexao, comum, origem["schema"], destino, manter_id=origem["id"], novo_id=novo_id)
    return codigo


def excluir_planejamento(engine: Engine, codigo: str) -> None:
    """Apaga o Planejamento e todo o seu schema. Irreversível."""
    registro = Planejamento.__table__
    with engine.begin() as conexao:
        linha = conexao.execute(select(registro.c.id, registro.c.schema).where(registro.c.codigo == codigo)).first()
        if linha is None:
            raise ErroDeCopia(f"Planejamento {codigo!r} não encontrado")
        if conexao.execute(select(text("count(*)")).select_from(registro)).scalar() <= 1:
            raise ErroDeCopia("Não é possível excluir o único Planejamento")
        conexao.execute(update(registro).where(registro.c.origem_id == linha.id).values(origem_id=None))
        conexao.execute(delete(PlanejamentoVersao.__table__).where(PlanejamentoVersao.__table__.c.planejamento_id == linha.id))
        if linha.schema:  # primeiro o schema: Volumes e Premissas dele ainda apontam para o registro
            conexao.execute(text(f"DROP SCHEMA {_q(linha.schema)} CASCADE"))
        conexao.execute(delete(registro).where(registro.c.id == linha.id))


def separar_legados(engine: Engine) -> list[str]:
    """Bancos anteriores ao Planejamento-contexto podem ter vários Planejamentos dividindo os mesmos dados (a migration
    deu o schema só ao primeiro). Dá a cada um dos demais o seu schema, cópia do primeiro com os seus volumes e premissas,
    e tira do primeiro o que era dos outros. Idempotente; devolve os códigos separados."""
    if not ps.e_postgres(engine):
        return []
    comum = ps.schema_comum(engine)
    registro = Planejamento.__table__
    separados: list[str] = []
    with engine.begin() as conexao:
        sem_schema = conexao.execute(select(registro.c.id, registro.c.codigo).where(registro.c.schema.is_(None)).order_by(registro.c.id)).all()
        base = conexao.execute(
            select(registro.c.id, registro.c.schema).where(registro.c.schema.is_not(None)).order_by(registro.c.id).limit(1)
        ).first()
        if not sem_schema or base is None:
            return []
        for legado in sem_schema:
            destino = ps.nome_do_schema(legado.id, comum)
            conexao.execute(update(registro).where(registro.c.id == legado.id).values(schema=destino))
            _clonar_schema(conexao, comum, base.schema, destino, manter_id=legado.id, novo_id=legado.id)
            separados.append(legado.codigo)
        for modelo in (VolumeProjetado, Premissa):
            conexao.execute(text(f'DELETE FROM {_q(base.schema)}."{modelo.__tablename__}" WHERE planejamento_id <> :id'), {"id": base.id})
    return separados
