"""
Interpretacao e comparacao de tags de imagem Docker.

As tags publicadas no registro da empresa nao seguem um padrao unico. Existem:

- versoes oficiais puras:            1.6.0, 2.10.3, 1.80
- versoes oficiais com plataforma:   5.4.9-linux, 1.4.2-windows-2016
- pre-lancamentos:                   0.9-beta, 1.6.0-rc1, 2.0.0-alpha.2
- tags de branch/feature:            FNX-5220-windows, master, minha-correcao

Este modulo define o que e "versao oficial", como ordenar duas tags e como
escolher a versao oficial mais recente de uma lista de tags.

Regras de ordenacao:

- a parte numerica inicial da tag manda (1.80 > 1.7.0 porque 80 > 7);
- componentes ausentes valem zero (1.80 é tratada como 1.80.0);
- sufixo de plataforma nao altera precedencia (5.4.9-linux == 5.4.9);
- pre-lancamento vale menos que a versao final (0.9-beta < 0.9);
- tag sem parte numerica (branch) vale menos que qualquer versao numerica e,
  entre elas, a ordem e alfabetica apenas para dar um resultado estavel.
"""

import re


# Tags que significam "use a versao oficial mais recente publicada no registro"
REFERENCIAS_VERSAO_MAIS_RECENTE = (
    "master",
    "main",
    "latest",
    "ultima",
    "mais-recente",
)

# Sufixos que apenas identificam a plataforma da imagem e nao a precedencia
SUFIXOS_PLATAFORMA = (
    "linux",
    "windows",
    "win",
    "alpine",
    "amd64",
    "arm64",
    "x64",
    "x86",
    "2016",
    "2019",
    "2022",
    "2025",
)

# Sufixos que identificam versao ainda nao liberada oficialmente
SUFIXOS_PRE_LANCAMENTO = (
    "alpha",
    "beta",
    "rc",
    "pre",
    "preview",
    "snapshot",
    "dev",
    "teste",
    "test",
    "homolog",
    "homologacao",
)

_INICIO_NUMERICO = re.compile(r"^(\d+(?:\.\d+)*)")

# Faixas de precedencia. A faixa manda antes de qualquer numero: numero de versao
# sempre acima de nome de branch. Pre-lancamento e versao, e nao faixa separada:
# a Central de Relatorios publicou 0.1.0-beta ... 0.4.0-beta como release oficial
# antes da 1.0.0. A marca de beta/rc so desempata o mesmo numero.
FAIXA_BRANCH = 0
FAIXA_VERSAO = 1


def eh_referencia_versao_mais_recente(valor):
    """Indica se a versao configurada pede a versao oficial mais recente."""

    if valor is None:
        return False

    return str(valor).strip().lower() in REFERENCIAS_VERSAO_MAIS_RECENTE


def _separar_tokens_sufixo(sufixo):
    if not sufixo:
        return []

    return [token for token in re.split(r"[-_.+]", sufixo.lower()) if token]


def _classificar_sufixo(sufixo):
    """Retorna (eh_pre_lancamento, tokens_desconhecidos) do sufixo da tag."""

    eh_pre_lancamento = False
    tokens_desconhecidos = []

    for token in _separar_tokens_sufixo(sufixo):
        # rc1, beta2, alpha.3 tambem contam como pre-lancamento
        token_sem_numero = re.sub(r"\d+$", "", token)

        if token in SUFIXOS_PRE_LANCAMENTO or token_sem_numero in SUFIXOS_PRE_LANCAMENTO:
            eh_pre_lancamento = True
        elif token in SUFIXOS_PLATAFORMA:
            continue
        elif token.isdigit():
            # numero solto depois do sufixo de plataforma (ex.: windows-2016)
            continue
        else:
            tokens_desconhecidos.append(token)

    return eh_pre_lancamento, tokens_desconhecidos


class VersaoDocker():
    """Tag de imagem Docker interpretada."""

    def __init__(self, tag):
        self.tag = "" if tag is None else str(tag).strip()

        correspondencia = _INICIO_NUMERICO.match(self.tag)

        if correspondencia:
            self.parte_numerica = correspondencia.group(1)
            self.sufixo = self.tag[len(self.parte_numerica):].lstrip("-_.+")
            self.numeros = tuple(int(parte) for parte in self.parte_numerica.split("."))
        else:
            self.parte_numerica = ""
            self.sufixo = self.tag
            self.numeros = tuple()

        self.eh_pre_lancamento, self.tokens_desconhecidos = _classificar_sufixo(self.sufixo)

    @property
    def eh_numerica(self):
        return len(self.numeros) > 0

    @property
    def parece_versao(self):
        """
        Numero de versao com sufixo so de plataforma, sem palavra estranha.

        Serve para separar versao (5.4.9, 5.4.9-linux, 0.9-beta) de nome de branch
        que comeca com numero (493081_junio_SacolaAdicionarProduto_Predial). Sem
        isso o numero da ficha entra na comparacao como se fosse versao, e 493081
        e maior que qualquer versao real.
        """

        return self.eh_numerica and not self.tokens_desconhecidos

    @property
    def faixa(self):
        """
        Faixa de precedencia: numero de versao > nome de branch.

        A faixa e comparada antes do numero, para nome de branch nunca aparecer
        acima de versao - inclusive branch de ficha, que comeca com numero.
        """

        return FAIXA_VERSAO if self.parece_versao else FAIXA_BRANCH

    @property
    def eh_versao_oficial(self):
        """Versao numerica, sem pre-lancamento e sem sufixo estranho de branch."""

        return self.parece_versao and not self.eh_pre_lancamento

    def numeros_normalizados(self, quantidade_componentes):
        return tuple(
            self.numeros[indice] if indice < len(self.numeros) else 0
            for indice in range(quantidade_componentes)
        )

    def plataformas(self):
        return [
            token for token in _separar_tokens_sufixo(self.sufixo)
            if token in SUFIXOS_PLATAFORMA
        ]

    def tem_plataforma(self, plataforma):
        if not plataforma:
            return True

        return str(plataforma).strip().lower() in self.plataformas()

    def __str__(self):
        return self.tag

    def __repr__(self):
        return f"VersaoDocker({self.tag!r})"


def analisar_versao(tag):
    return VersaoDocker(tag)


def _sinal(valor):
    return (valor > 0) - (valor < 0)


def _comparar_texto(texto1, texto2):
    return _sinal((texto1 > texto2) - (texto1 < texto2))


def _comparar_numeros(versao1, versao2):
    """Compara a parte numerica componente por componente, ausente valendo zero."""

    quantidade_componentes = max(len(versao1.numeros), len(versao2.numeros))

    numeros1 = versao1.numeros_normalizados(quantidade_componentes)
    numeros2 = versao2.numeros_normalizados(quantidade_componentes)

    if numeros1 == numeros2:
        return 0

    return 1 if numeros1 > numeros2 else -1


def comparar_versoes(tag1, tag2):
    """Compara duas tags. Retorna -1, 0 ou 1 (tag1 menor, igual ou maior)."""

    versao1 = tag1 if isinstance(tag1, VersaoDocker) else VersaoDocker(tag1)
    versao2 = tag2 if isinstance(tag2, VersaoDocker) else VersaoDocker(tag2)

    if versao1.faixa != versao2.faixa:
        return 1 if versao1.faixa > versao2.faixa else -1

    if versao1.faixa == FAIXA_BRANCH:
        # Entre branches, prefixo numerico ainda ordena (ficha mais nova primeiro)
        if versao1.eh_numerica != versao2.eh_numerica:
            return 1 if versao1.eh_numerica else -1

        if versao1.eh_numerica:
            diferenca = _comparar_numeros(versao1, versao2)

            if diferenca != 0:
                return diferenca

        return _comparar_texto(versao1.tag.lower(), versao2.tag.lower())

    diferenca = _comparar_numeros(versao1, versao2)

    if diferenca != 0:
        return diferenca

    if versao1.eh_pre_lancamento != versao2.eh_pre_lancamento:
        return -1 if versao1.eh_pre_lancamento else 1

    if versao1.eh_pre_lancamento and versao1.sufixo.lower() != versao2.sufixo.lower():
        return 1 if versao1.sufixo.lower() > versao2.sufixo.lower() else -1

    return 0


def versao_maior_ou_igual(tag, tag_minima):
    return comparar_versoes(tag, tag_minima) >= 0


def pode_comparar_com_versao_minima(tag):
    """
    Só faz sentido aplicar regra de versao minima sobre numero de versao.

    Versao de teste fica de fora, e nao basta olhar se a tag comeca com numero:
    branch de ficha comeca com o numero da ficha
    (493081_junio_SacolaAdicionarProduto_Predial) e seria comparada como se fosse
    a versao 493081, que e maior que qualquer versao real - fazendo o mapa de
    dependencias cobrar (ou liberar) regra que nao tem como avaliar.

    Pre-lancamento continua comparavel: 0.9-beta carrega o contrato da 0.9, e
    ignorar a regra nesse caso desligaria a validacao numa implantacao de teste
    que justamente precisa dela.
    """

    return VersaoDocker(tag).parece_versao


def listar_versoes_oficiais(tags, plataforma=None, limite=None):
    """
    Versoes oficiais da lista de tags, da mais recente para a mais antiga.

    Usada para sugerir versoes ao usuario. Tags de branch e pre-lancamentos ficam
    de fora de proposito: sugerir uma imagem de teste como se fosse opcao valida
    de implantacao seria pior que nao sugerir nada.
    """

    oficiais = [
        tag for tag in (tags or [])
        if analisar_versao(tag).eh_versao_oficial
        and (not plataforma or analisar_versao(tag).tem_plataforma(plataforma))
    ]

    ordenadas = ordenar_tags_da_mais_recente_para_mais_antiga(oficiais)

    return ordenadas if limite is None else ordenadas[:limite]


def selecionar_versao_oficial_mais_recente(tags, plataforma=None):
    """
    Escolhe a versao oficial mais recente da lista de tags.

    Quando `plataforma` e informada (ex.: "linux"), apenas tags com esse sufixo
    de plataforma sao consideradas. Isso evita escolher uma tag `-windows` para
    um deploy Linux.
    """

    candidatas = []

    for tag in tags or []:
        versao = VersaoDocker(tag)

        if not versao.eh_versao_oficial:
            continue

        if plataforma and not versao.tem_plataforma(plataforma):
            continue

        candidatas.append(versao)

    if not candidatas:
        return None

    melhor = candidatas[0]

    for candidata in candidatas[1:]:
        if comparar_versoes(candidata, melhor) > 0:
            melhor = candidata

    return melhor.tag


def ordenar_tags_da_mais_recente_para_mais_antiga(tags):
    import functools

    return sorted(
        [tag for tag in (tags or []) if tag],
        key=functools.cmp_to_key(comparar_versoes),
        reverse=True
    )
