"""Um *schema* do Postgres por Planejamento: roteamento da sessão e cópia.

O Planejamento ("2026 Vrs. 01") guarda tudo — cadastros, formulações, alocações, despesas, impostos, Centros de Resultado,
volumes, premissas, tabelas e custeios. Em vez de uma coluna `planejamento_id` em cada tabela, cada Planejamento tem o seu
*schema* (`plan_<id>`) com cópias independentes das tabelas; só as `COMPARTILHADAS` existem uma vez, no schema comum.

A sessão é apontada para o schema do Planejamento por `SET LOCAL search_path TO plan_x, <comum>` no início de **cada**
transação (os módulos fazem vários `commit()` por requisição, então um `SET` único se perderia). No SQLite dos testes
não há schemas: nada acontece e há um único conjunto de dados.
"""

import re
from functools import lru_cache

from sqlalchemy import Engine, event, inspect
from sqlalchemy.engine import Connection
from sqlalchemy.orm import Session

# Existem uma vez só, no schema comum: usuários, log, o registro dos Planejamentos e as fotos antigas.
COMPARTILHADAS = frozenset({"usuario", "registro_atividade", "planejamento", "planejamento_versao", "alembic_version"})

_SCHEMA_VALIDO = re.compile(r"^[a-z][a-z0-9_]{0,62}$")


def e_postgres(bind: Engine | Connection) -> bool:
    return bind.dialect.name == "postgresql"


def nome_do_schema(planejamento_id: int, comum: str) -> str:
    """`plan_<id>`; fora do schema padrão (`public`) leva o nome dele como prefixo, porque nomes de schema são do banco todo
    (simulações e testes compartilham o banco de desenvolvimento)."""
    return f"plan_{planejamento_id}" if comum == "public" else f"{comum}_plan_{planejamento_id}"


def validar_schema(schema: str) -> str:
    """O nome entra em SQL (identificadores não aceitam parâmetro): só letras minúsculas, números e '_'."""
    if not _SCHEMA_VALIDO.match(schema):
        raise ValueError(f"Nome de schema inválido: {schema!r}")
    return schema


@lru_cache(maxsize=8)
def _schema_comum_de(url: str, engine: Engine) -> str:
    return inspect(engine).default_schema_name or "public"


def schema_comum(engine: Engine) -> str:
    """O schema onde ficam as tabelas compartilhadas (o `search_path` padrão da conexão)."""
    return _schema_comum_de(str(engine.url), engine)


def tabelas_do_planejamento():
    """As tabelas que existem uma vez por Planejamento, em ordem de dependência (pais antes dos filhos)."""
    from custos_precificacao.models import Base

    return [t for t in Base.metadata.sorted_tables if t.name not in COMPARTILHADAS]


def _apontar_transacao(session: Session, transaction, connection: Connection) -> None:
    schema = session.info.get("schema")
    if schema and e_postgres(connection):
        comum = schema_comum(connection.engine)
        connection.exec_driver_sql(f'SET LOCAL search_path TO "{validar_schema(schema)}", "{validar_schema(comum)}"')


event.listen(Session, "after_begin", _apontar_transacao)


def apontar(session: Session, schema: str | None) -> None:
    """Direciona a sessão para o schema (None = sem roteamento). Vale a partir da próxima transação: encerre a atual antes."""
    if schema:
        session.info["schema"] = validar_schema(schema)
    else:
        session.info.pop("schema", None)


def planejamento_padrao(engine: Engine):
    """O Planejamento usado quando nenhum é pedido: o oficial; sem oficial, o mais recente. Só considera os que têm schema."""
    from sqlalchemy import select

    from custos_precificacao.models import Planejamento

    tabela = Planejamento.__table__
    consulta = (
        select(tabela.c.id, tabela.c.codigo, tabela.c.schema)
        .where(tabela.c.schema.is_not(None))
        .order_by(tabela.c.oficial.desc(), tabela.c.id.desc())
        .limit(1)
    )
    with engine.connect() as conexao:
        return conexao.execute(consulta).first()


def planejamento_por_codigo(engine: Engine, codigo: str):
    from sqlalchemy import select

    from custos_precificacao.models import Planejamento

    tabela = Planejamento.__table__
    with engine.connect() as conexao:
        return conexao.execute(select(tabela.c.id, tabela.c.codigo, tabela.c.schema).where(tabela.c.codigo == codigo)).first()
