"""
Mapa de dependencias entre versoes dos sistemas.

Le as regras de `mapa-dependencias.yaml` e verifica se as versoes que serao
implantadas formam uma combinacao valida.

Uma regra diz: "a partir da versao X do sistema A, o sistema B precisa estar na
versao Y ou superior". Quando marcada como bidirecional, a reciproca tambem
vale, ou seja, os dois sistemas precisam cruzar o limite na mesma implantacao.
"""

import os

import yaml

from lib.echo import echo
from lib.sistemas_docker import SISTEMAS_DOCKER_POR_CHAVE
from lib.versoes_docker import (
    pode_comparar_com_versao_minima,
    versao_maior_ou_igual,
)


NOME_ARQUIVO_MAPA_DEPENDENCIAS = "mapa-dependencias.yaml"


class ErroMapaDependencias(Exception):
    """Mapa de dependencias ausente ou mal formado."""


class LadoRegra():
    def __init__(self, sistema, versao_minima):
        self.sistema = sistema
        self.versao_minima = versao_minima

    def nome_sistema(self):
        if self.sistema in SISTEMAS_DOCKER_POR_CHAVE:
            return SISTEMAS_DOCKER_POR_CHAVE[self.sistema].nome

        return self.sistema


class RegraDependencia():
    def __init__(self, identificador, descricao, quando, exige, bidirecional=False):
        self.identificador = identificador
        self.descricao = descricao
        self.quando = quando
        self.exige = exige
        self.bidirecional = bidirecional

    def sistemas(self):
        return (self.quando.sistema, self.exige.sistema)


class ViolacaoDependencia():
    def __init__(self, regra, lado_atendido, lado_pendente, versao_lado_atendido, versao_lado_pendente):
        self.regra = regra
        self.lado_atendido = lado_atendido
        self.lado_pendente = lado_pendente
        self.versao_lado_atendido = versao_lado_atendido
        self.versao_lado_pendente = versao_lado_pendente

    def mensagem(self):
        return (
            f"Combinacao de versoes invalida (regra {self.regra.identificador}): "
            f"{self.lado_atendido.nome_sistema()} na versao {self.versao_lado_atendido} "
            f"exige {self.lado_pendente.nome_sistema()} na versao "
            f"{self.lado_pendente.versao_minima} ou superior, mas a versao "
            f"configurada e {self.versao_lado_pendente}. {self.regra.descricao}"
        )

    def __str__(self):
        return self.mensagem()


def obter_caminho_mapa_dependencias(diretorio_scripts_implantacao):
    return os.path.join(diretorio_scripts_implantacao, NOME_ARQUIVO_MAPA_DEPENDENCIAS)


def _ler_lado_regra(dados, nome_campo, identificador_regra):
    if nome_campo not in dados or not isinstance(dados[nome_campo], dict):
        raise ErroMapaDependencias(
            f"Regra '{identificador_regra}' do mapa de dependencias nao tem o bloco '{nome_campo}'."
        )

    lado = dados[nome_campo]

    if "sistema" not in lado or not lado["sistema"]:
        raise ErroMapaDependencias(
            f"Regra '{identificador_regra}' nao informou '{nome_campo}.sistema'."
        )

    if "versaoMinima" not in lado or not lado["versaoMinima"]:
        raise ErroMapaDependencias(
            f"Regra '{identificador_regra}' nao informou '{nome_campo}.versaoMinima'."
        )

    sistema = str(lado["sistema"]).strip()

    if sistema not in SISTEMAS_DOCKER_POR_CHAVE:
        raise ErroMapaDependencias(
            f"Regra '{identificador_regra}' referencia o sistema desconhecido '{sistema}'. "
            f"Sistemas validos: {', '.join(sorted(SISTEMAS_DOCKER_POR_CHAVE))}."
        )

    return LadoRegra(sistema, str(lado["versaoMinima"]).strip())


def interpretar_mapa_dependencias(dados):
    """Converte o conteudo do YAML em regras validadas."""

    if not dados:
        return []

    if not isinstance(dados, dict):
        raise ErroMapaDependencias("Mapa de dependencias invalido: esperado um mapa na raiz.")

    regras_brutas = dados["regras"] if "regras" in dados and dados["regras"] else []

    if not isinstance(regras_brutas, list):
        raise ErroMapaDependencias("Mapa de dependencias invalido: 'regras' precisa ser uma lista.")

    regras = []
    identificadores = set()

    for indice, regra_bruta in enumerate(regras_brutas):
        if not isinstance(regra_bruta, dict):
            raise ErroMapaDependencias(
                f"Regra na posicao {indice + 1} do mapa de dependencias nao e um mapa."
            )

        identificador = (
            str(regra_bruta["id"]).strip()
            if "id" in regra_bruta and regra_bruta["id"]
            else f"regra-{indice + 1}"
        )

        if identificador in identificadores:
            raise ErroMapaDependencias(
                f"Mapa de dependencias tem mais de uma regra com id '{identificador}'."
            )

        identificadores.add(identificador)

        descricao = (
            str(regra_bruta["descricao"]).strip()
            if "descricao" in regra_bruta and regra_bruta["descricao"]
            else ""
        )

        bidirecional = (
            bool(regra_bruta["bidirecional"])
            if "bidirecional" in regra_bruta
            else False
        )

        regras.append(
            RegraDependencia(
                identificador=identificador,
                descricao=descricao,
                quando=_ler_lado_regra(regra_bruta, "quando", identificador),
                exige=_ler_lado_regra(regra_bruta, "exige", identificador),
                bidirecional=bidirecional
            )
        )

    return regras


def carregar_mapa_dependencias(diretorio_scripts_implantacao):
    """Carrega as regras do arquivo. Retorna lista vazia quando o arquivo nao existe."""

    caminho = obter_caminho_mapa_dependencias(diretorio_scripts_implantacao)

    if not os.path.exists(caminho):
        echo(
            f"[WARN] Mapa de dependencias nao encontrado em {caminho}. "
            "As dependencias entre versoes nao serao validadas.",
            "warn"
        )
        return []

    try:
        with open(caminho, "r", encoding="utf-8") as arquivo:
            dados = yaml.load(arquivo, Loader=yaml.FullLoader)
    except ErroMapaDependencias:
        raise
    except Exception as erro:
        raise ErroMapaDependencias(f"Falha ao ler o mapa de dependencias {caminho}: {erro}")

    return interpretar_mapa_dependencias(dados)


def _avaliar_sentido(regra, lado_origem, lado_destino, versoes):
    """
    Verifica um sentido da regra.

    Se a versao do sistema de origem alcancou a versao minima dele, a versao do
    sistema de destino tambem precisa ter alcancado a versao minima dela.
    """

    versao_origem = versoes[lado_origem.sistema]
    versao_destino = versoes[lado_destino.sistema]

    if not versao_maior_ou_igual(versao_origem, lado_origem.versao_minima):
        return None

    if versao_maior_ou_igual(versao_destino, lado_destino.versao_minima):
        return None

    return ViolacaoDependencia(
        regra=regra,
        lado_atendido=lado_origem,
        lado_pendente=lado_destino,
        versao_lado_atendido=versao_origem,
        versao_lado_pendente=versao_destino
    )


def validar_dependencias(versoes_por_sistema, regras, chave_sistema=None):
    """
    Avalia as regras contra as versoes informadas.

    `versoes_por_sistema` mapeia chave do sistema para a versao concreta que
    sera implantada. Sistemas ausentes do mapa sao tratados como nao
    configurados e suas regras sao ignoradas.

    Quando `chave_sistema` e informada, apenas as regras que envolvem esse
    sistema sao avaliadas.

    Retorna a lista de violacoes encontradas.
    """

    violacoes = []

    for regra in regras or []:
        if chave_sistema and chave_sistema not in regra.sistemas():
            continue

        sistemas_da_regra = regra.sistemas()

        if any(sistema not in versoes_por_sistema for sistema in sistemas_da_regra):
            continue

        versoes_nao_comparaveis = [
            sistema for sistema in sistemas_da_regra
            if not pode_comparar_com_versao_minima(versoes_por_sistema[sistema])
        ]

        if versoes_nao_comparaveis:
            for sistema in versoes_nao_comparaveis:
                echo(
                    f"[WARN] Regra de dependencia {regra.identificador} ignorada: a versao "
                    f"'{versoes_por_sistema[sistema]}' do sistema "
                    f"{SISTEMAS_DOCKER_POR_CHAVE[sistema].nome} nao e um numero de versao "
                    "(branch ou versao de teste) e nao pode ser comparada com a versao "
                    "minima da regra.",
                    "warn"
                )
            continue

        violacao = _avaliar_sentido(regra, regra.quando, regra.exige, versoes_por_sistema)

        if violacao:
            violacoes.append(violacao)
            continue

        if regra.bidirecional:
            violacao = _avaliar_sentido(regra, regra.exige, regra.quando, versoes_por_sistema)

            if violacao:
                violacoes.append(violacao)

    return violacoes
