/*
listarUltimasVersoes.js

Percorre todo o catalogo de sistemas distribuidos pelos scripts de implantacao e
mostra, para cada um: o nome do projeto, a ultima versao publicada, a data de
geracao dessa versao e o endereco de localizacao (imagem no registro Docker,
repositorio Git ou pasta de download).

A saida em --json traz ainda o projeto no GitLab (id, caminho e url), os links de
listagem consultados, o digest e o tamanho da imagem.

Uso:
  node listarUltimasVersoes.js <token-gitlab>
  node listarUltimasVersoes.js <token-gitlab> --json
  node listarUltimasVersoes.js <token-gitlab> --filtro enterprise
  node listarUltimasVersoes.js <token-gitlab> --storybooks

Os storybooks ficam de fora por padrao, porque sao vitrine de componentes e nao
sistema implantado no cliente. Passe --storybooks para inclui-los.

O token tambem pode vir da variavel de ambiente GITLAB_TOKEN. Precisa dos
escopos read_api e read_registry.
*/

const axios = require('axios').default

const { selecionarUltimaVersao } = require('./ordenacaoVersoes')

const GITLAB = 'https://gitlab.microuniverso.com.br'
const API = `${GITLAB}/api/v4`
const REGISTRO = 'gitlab.microuniverso.com.br:5050'

// ---------------------------------------------------------------------------
// Catalogo
// ---------------------------------------------------------------------------
// tipo "docker": imagem = caminho da imagem sem o host do registro. O caminho
//                do projeto no GitLab e a imagem sem o ultimo trecho, entao o
//                script consegue resolver o projeto mesmo sem o id. projeto e
//                repositorio sao os ids ja usados nos scripts
//                listarImagensETagsExistentes.js de hoje (usados como atalho e
//                conferidos contra o que a API devolve).
// tipo "git":    versao vem das tags (e opcionalmente branches) do repositorio.
// tipo "http":   versao vem da listagem de arquivos no site de download.

const CATALOGO = [
    {
        tipo: 'docker',
        chave: 'enterprise',
        nome: 'API Enterprise',
        imagem: 'microuniverso/mufenix/enterprise/enterprise',
        projeto: 49,
        repositorio: 5,
        plataformas: ['linux', 'windows']
    },
    {
        tipo: 'docker',
        chave: 'enterpriseRenderizador',
        nome: 'Renderizador da API Enterprise',
        imagem: 'microuniverso/mufenix/enterprise/renderizador',
        projeto: 49
    },
    {
        tipo: 'docker',
        chave: 'centralDeCadastros',
        nome: 'Central de Cadastros',
        imagem: 'microuniverso/mufenix/centraldecadastrosfrontend/central-de-cadastros-producao',
        projeto: 58,
        repositorio: 6
    },
    {
        tipo: 'docker',
        chave: 'centralDeCadastrosStorybook',
        nome: 'Storybook da Central de Cadastros',
        imagem: 'microuniverso/mufenix/centraldecadastrosfrontend/central-de-cadastros-storybook',
        projeto: 58,
        repositorio: 9,
        storybook: true
    },
    {
        tipo: 'docker',
        chave: 'centralDeRelatorios',
        nome: 'Central de Relatorios',
        imagem: 'microuniverso/mufenix/centralrelatorios/central-de-relatorios',
        projeto: 103,
        repositorio: 16
    },
    {
        tipo: 'docker',
        chave: 'centralDeRelatoriosStorybook',
        nome: 'Storybook da Central de Relatorios',
        imagem: 'microuniverso/mufenix/centralrelatorios/central-de-relatorios-storybook',
        projeto: 103,
        storybook: true
    },
    {
        tipo: 'docker',
        chave: 'pesquisaProduto',
        nome: 'Pesquisa de Produto',
        imagem: 'microuniverso/mufenix/pesquisaproduto/pesquisa-produto',
        projeto: 88,
        repositorio: 7
    },
    {
        tipo: 'docker',
        chave: 'coletaPrecos',
        nome: 'Coleta de Precos',
        imagem: 'microuniverso/mufenix/coletaprecos/coleta-preco',
        projeto: 97,
        repositorio: 15
    },
    {
        tipo: 'docker',
        chave: 'coletaPrecosStorybook',
        nome: 'Storybook da Coleta de Precos',
        imagem: 'microuniverso/mufenix/coletaprecos/coleta-preco-storybook',
        projeto: 97,
        repositorio: 5,
        storybook: true
    },
    {
        tipo: 'docker',
        chave: 'intercompany',
        nome: 'Intercompany',
        imagem: 'microuniverso/mufenix/intercompanyfrontend/inter-company-frontend-releases',
        projeto: 93,
        repositorio: 13
    },
    {
        tipo: 'docker',
        chave: 'intercompanyStorybook',
        nome: 'Storybook do Intercompany',
        imagem: 'microuniverso/mufenix/intercompanyfrontend/inter-company-storybook-releases',
        projeto: 93,
        repositorio: 14,
        storybook: true
    },
    {
        tipo: 'docker',
        chave: 'configuracoesWeb',
        nome: 'Configuracoes Web',
        imagem: 'microuniverso/mufenix/configuracoesweb/configuracoes-web',
        projeto: 114,
        repositorio: 23
    },
    {
        tipo: 'docker',
        chave: 'muWhatsapp',
        nome: 'Mu Whatsapp API',
        imagem: 'microuniverso/mufenix/wppconnect-server/muwhatsapp',
        projeto: 110,
        repositorio: 20
    },
    {
        tipo: 'docker',
        chave: 'microkafka',
        nome: 'Microkafka',
        imagem: 'microuniverso/mufenix/microkafka/microkafka',
        projeto: 33,
        repositorio: 3
    },
    {
        tipo: 'docker',
        chave: 'microkafkaConfigurador',
        nome: 'Configurador do Microkafka',
        imagem: 'microuniverso/mufenix/microkafka/configurar-microkafka',
        projeto: 33,
        repositorio: 4
    },
    {
        tipo: 'docker',
        chave: 'vendasStorybook',
        nome: 'Storybook de Vendas (Orcamento Web)',
        imagem: 'microuniverso/mufenix/projetoweb/vendas-storybook',
        projeto: 78,
        repositorio: 8,
        storybook: true
    },
    {
        tipo: 'docker',
        chave: 'cassini',
        nome: 'Cassini',
        imagem: 'microuniverso/mufenix/cassini/cassini'
    },
    {
        tipo: 'docker',
        chave: 'proxyHttpsLinux',
        nome: 'Proxy HTTPS Linux',
        imagem: 'microuniverso/mufenix/scriptsdeploy/proxyhttpslinux'
    },
    {
        tipo: 'git',
        chave: 'releaseWeb',
        nome: 'Microuniverso Web / Sites IIS (ReleaseWeb)',
        projeto: 77,
        incluirBranches: true
    },
    {
        tipo: 'git',
        chave: 'muDllRest',
        nome: 'MuDllRest',
        projeto: 90
    },
    {
        tipo: 'git',
        chave: 'whatsappBot',
        nome: 'Whatsapp Bot',
        projeto: 19,
        usarSomenteBranches: true
    },
    {
        tipo: 'http',
        chave: 'srvcusto',
        nome: 'SRVCUSTO (utilitario TradSql)',
        listagem: 'http://download.microuniverso.com.br/TradSql/Utilitarios/SRVCUSTO/',
        regex: /href="(srvcusto(\d+)\.zip)"/g
    }
]

// Sistemas cujo endereco e definido no YAML do cliente ou que usam imagem
// publica: nao ha listagem propria no GitLab da empresa.
const SEM_ENDERECO_FIXO = [
    {
        chave: 'monitorFreteRapido',
        nome: 'Monitor Frete Rapido',
        observacao: 'imagem e versao vem de monitorFreteRapido.imagem.nome / .versao no YAML do cliente'
    },
    {
        chave: 'mongoDbSitesIIS',
        nome: 'MongoDB dos Sites IIS',
        observacao: 'usa a imagem publica mongo:4.4'
    },
    {
        chave: 'apiGateway',
        nome: 'API Gateway',
        observacao: 'usa a imagem publica nginx:1.26.0'
    }
]

// ---------------------------------------------------------------------------
// Acesso ao GitLab
// ---------------------------------------------------------------------------

function criarCliente(token) {
    return axios.create({
        baseURL: API,
        headers: { 'PRIVATE-TOKEN': token },
        timeout: 60000
    })
}

async function listarPaginado(cliente, caminho) {
    let itens = []
    let pagina = 1
    let totalPaginas = 1

    do {
        const resposta = await cliente.get(caminho, { params: { per_page: 100, page: pagina } })

        itens = [...itens, ...resposta.data]

        totalPaginas = Number(resposta.headers['x-total-pages'] || 1)
        pagina++
    } while (pagina <= totalPaginas && pagina <= 50)

    return itens
}

function caminhoProjetoDaImagem(imagem) {
    const partes = imagem.split('/')

    return partes.slice(0, partes.length - 1).join('/')
}

async function resolverProjeto(cliente, entrada) {
    const identificadores = []

    if (entrada.projeto) {
        identificadores.push(String(entrada.projeto))
    }

    if (entrada.imagem) {
        identificadores.push(encodeURIComponent(caminhoProjetoDaImagem(entrada.imagem)))
    }

    let ultimoErro = null

    for (const identificador of identificadores) {
        try {
            const resposta = await cliente.get(`/projects/${identificador}`)

            return resposta.data
        } catch (erro) {
            ultimoErro = erro
        }
    }

    throw ultimoErro || new Error('projeto nao informado no catalogo')
}

async function resolverRepositorioDoRegistro(cliente, entrada, projeto) {
    const repositorios = await listarPaginado(cliente, `/projects/${projeto.id}/registry/repositories`)

    const localizacaoEsperada = `${REGISTRO}/${entrada.imagem}`.toLowerCase()

    const porLocalizacao = repositorios.find(r => String(r.location).toLowerCase() === localizacaoEsperada)

    if (porLocalizacao) {
        const divergencia = entrada.repositorio && entrada.repositorio !== porLocalizacao.id
            ? `id de repositorio configurado nos scripts (${entrada.repositorio}) e diferente do id real (${porLocalizacao.id})`
            : null

        return { repositorio: porLocalizacao, divergencia }
    }

    const porId = repositorios.find(r => r.id === entrada.repositorio)

    if (porId) {
        return {
            repositorio: porId,
            divergencia: `imagem esperada (${localizacaoEsperada}) nao existe no projeto; usando o repositorio de id ${porId.id} (${porId.location})`
        }
    }

    throw new Error(`nenhum repositorio do registro casou com ${localizacaoEsperada} no projeto ${projeto.path_with_namespace}`)
}

async function obterDetalheDaTag(cliente, projetoId, repositorioId, tag) {
    const resposta = await cliente.get(`/projects/${projetoId}/registry/repositories/${repositorioId}/tags/${encodeURIComponent(tag)}`)

    return resposta.data
}

// ---------------------------------------------------------------------------
// Coleta por tipo
// ---------------------------------------------------------------------------

async function coletarDocker(cliente, entrada) {
    const projeto = await resolverProjeto(cliente, entrada)
    const { repositorio, divergencia } = await resolverRepositorioDoRegistro(cliente, entrada, projeto)

    const linkListagem = `${API}/projects/${projeto.id}/registry/repositories/${repositorio.id}/tags`

    const tags = await listarPaginado(cliente, `/projects/${projeto.id}/registry/repositories/${repositorio.id}/tags`)

    const nomes = tags.map(t => t.name).filter(Boolean)

    const plataformas = entrada.plataformas || [null]

    const versoes = []

    for (const plataforma of plataformas) {
        const nomeTag = selecionarUltimaVersao(nomes, plataforma)

        if (nomeTag == null) {
            versoes.push({ plataforma, versao: null, geradaEm: null, erro: 'nenhuma tag encontrada' })
            continue
        }

        let detalhe = {}

        try {
            detalhe = await obterDetalheDaTag(cliente, projeto.id, repositorio.id, nomeTag)
        } catch (erro) {
            detalhe = { erro: mensagemDeErro(erro) }
        }

        versoes.push({
            plataforma,
            versao: nomeTag,
            geradaEm: detalhe.created_at || null,
            digest: detalhe.digest || null,
            tamanhoBytes: detalhe.total_size || null,
            imagemCompleta: detalhe.location || `${repositorio.location}:${nomeTag}`,
            erro: detalhe.erro || null
        })
    }

    return {
        chave: entrada.chave,
        nome: entrada.nome,
        tipo: 'docker',
        projeto: { id: projeto.id, nome: projeto.name, caminho: projeto.path_with_namespace, url: projeto.web_url },
        localizacao: repositorio.location,
        totalDeTags: tags.length,
        linkListagem,
        divergencia: divergencia || null,
        versoes
    }
}

async function coletarGit(cliente, entrada) {
    const projeto = await resolverProjeto(cliente, entrada)

    const linkTags = `${API}/projects/${projeto.id}/repository/tags`
    const linkBranches = `${API}/projects/${projeto.id}/repository/branches`

    const usarTags = !entrada.usarSomenteBranches
    const usarBranches = Boolean(entrada.incluirBranches || entrada.usarSomenteBranches)

    const tags = usarTags ? await listarPaginado(cliente, `/projects/${projeto.id}/repository/tags`) : []
    const branches = usarBranches ? await listarPaginado(cliente, `/projects/${projeto.id}/repository/branches`) : []

    const candidatos = [
        ...tags.map(t => ({
            name: t.name,
            origem: 'tag',
            data: (t.commit && (t.commit.created_at || t.commit.committed_date)) || null
        })),
        ...branches.map(b => ({
            name: b.name,
            origem: 'branch',
            data: (b.commit && (b.commit.committed_date || b.commit.created_at)) || null
        }))
    ]

    // A tag ganha da branch de mesmo nome, porque entra primeiro na lista
    const porNome = new Map()

    for (const candidato of candidatos) {
        if (candidato.name && !porNome.has(candidato.name)) {
            porNome.set(candidato.name, candidato)
        }
    }

    const nomeUltima = selecionarUltimaVersao([...porNome.keys()])
    const ultima = nomeUltima == null ? null : porNome.get(nomeUltima)

    return {
        chave: entrada.chave,
        nome: entrada.nome,
        tipo: 'git',
        projeto: { id: projeto.id, nome: projeto.name, caminho: projeto.path_with_namespace, url: projeto.web_url },
        localizacao: projeto.http_url_to_repo || projeto.web_url,
        totalDeTags: tags.length,
        totalDeBranches: branches.length,
        linksListagem: [usarTags ? linkTags : null, usarBranches ? linkBranches : null].filter(Boolean),
        versoes: ultima == null
            ? [{ plataforma: null, versao: null, geradaEm: null, erro: 'nenhuma tag/branch encontrada' }]
            : [{ plataforma: null, versao: ultima.name, geradaEm: ultima.data, origem: ultima.origem }]
    }
}

async function coletarHttp(entrada) {
    const resposta = await axios.get(entrada.listagem, { timeout: 60000 })

    const encontrados = []
    let match

    entrada.regex.lastIndex = 0

    while ((match = entrada.regex.exec(resposta.data)) !== null) {
        encontrados.push({ arquivo: match[1], name: match[2] })
    }

    const ordenados = [...encontrados].sort((a, b) => Number(a.name) - Number(b.name)).reverse()
    const ultimo = ordenados[0]

    let geradaEm = null

    if (ultimo) {
        try {
            const cabecalho = await axios.head(`${entrada.listagem}${ultimo.arquivo}`, { timeout: 60000 })

            geradaEm = cabecalho.headers['last-modified'] || null
        } catch (erro) {
            geradaEm = null
        }
    }

    return {
        chave: entrada.chave,
        nome: entrada.nome,
        tipo: 'http',
        projeto: null,
        localizacao: ultimo ? `${entrada.listagem}${ultimo.arquivo}` : entrada.listagem,
        totalDeVersoes: encontrados.length,
        linkListagem: entrada.listagem,
        versoes: ultimo == null
            ? [{ plataforma: null, versao: null, geradaEm: null, erro: 'nenhum arquivo encontrado' }]
            : [{ plataforma: null, versao: ultimo.name, geradaEm }]
    }
}

function mensagemDeErro(erro) {
    if (erro && erro.response) {
        return `HTTP ${erro.response.status} em ${erro.config ? erro.config.url : '?'}`
    }

    return erro && erro.message ? erro.message : String(erro)
}

async function coletar(cliente, entrada) {
    try {
        if (entrada.tipo === 'docker') {
            return await coletarDocker(cliente, entrada)
        }

        if (entrada.tipo === 'git') {
            return await coletarGit(cliente, entrada)
        }

        return await coletarHttp(entrada)
    } catch (erro) {
        return {
            chave: entrada.chave,
            nome: entrada.nome,
            tipo: entrada.tipo,
            localizacao: entrada.imagem ? `${REGISTRO}/${entrada.imagem}` : null,
            linkListagem: entrada.listagem || null,
            erro: mensagemDeErro(erro),
            versoes: []
        }
    }
}

// ---------------------------------------------------------------------------
// Saida
// ---------------------------------------------------------------------------

function formatarData(valor) {
    if (!valor) {
        return '-'
    }

    const data = new Date(valor)

    if (isNaN(data.getTime())) {
        return String(valor)
    }

    const p = n => String(n).padStart(2, '0')

    return `${p(data.getDate())}/${p(data.getMonth() + 1)}/${data.getFullYear()} ${p(data.getHours())}:${p(data.getMinutes())}`
}

function imprimirTexto(resultados) {
    for (const r of resultados) {
        console.log('')
        console.log(`== ${r.nome}  [${r.chave}]`)

        if (r.erro) {
            console.log(`   erro ......... ${r.erro}`)
        }

        for (const v of r.versoes) {
            const rotulo = v.plataforma ? `ultima versao (${v.plataforma})` : 'ultima versao'

            if (!v.versao) {
                console.log(`   ${rotulo} .. ${v.erro || 'nao encontrada'}`)
                continue
            }

            const origem = v.origem ? ` [${v.origem}]` : ''

            console.log(`   ${rotulo} .. ${v.versao}${origem}   gerada em ${formatarData(v.geradaEm)}`)

        }

        if (r.localizacao) {
            console.log(`   localizacao .. ${r.localizacao}`)
        }

        if (r.divergencia) {
            console.log(`   atencao ...... ${r.divergencia}`)
        }
    }

    console.log('')
    console.log('== Sem endereco fixo no repositorio')

    for (const s of SEM_ENDERECO_FIXO) {
        console.log(`   ${s.nome} [${s.chave}]: ${s.observacao}`)
    }

    console.log('')
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

async function main() {
    const argumentos = process.argv.slice(2)
    const posicionais = argumentos.filter(a => !a.startsWith('--'))

    const json = argumentos.includes('--json')
    const storybooks = argumentos.includes('--storybooks')

    const indiceFiltro = argumentos.indexOf('--filtro')
    const filtro = indiceFiltro >= 0 ? argumentos[indiceFiltro + 1] : null

    const token = posicionais.filter(a => a !== filtro)[0] || process.env.GITLAB_TOKEN

    if (!token) {
        console.error('token nao informado. Use: node listarUltimasVersoes.js <token> [--json] [--filtro texto]')
        process.exitCode = 1
        return
    }

    const cliente = criarCliente(token)

    // Storybook so entra com a flag, inclusive quando o filtro casaria com ele:
    // a flag manda, para o resultado nao depender de como o filtro foi escrito.
    let entradas = CATALOGO.filter(e => storybooks || !e.storybook)

    const casaComFiltro = (e, alvo) =>
        e.chave.toLowerCase().includes(alvo) ||
        e.nome.toLowerCase().includes(alvo) ||
        String(e.imagem || '').toLowerCase().includes(alvo)

    if (filtro) {
        const alvo = filtro.toLowerCase()

        entradas = entradas.filter(e => casaComFiltro(e, alvo))

        if (entradas.length === 0) {
            const haStorybook = CATALOGO.some(e => e.storybook && casaComFiltro(e, alvo))

            console.error(haStorybook && !storybooks
                ? `nenhum item casou com o filtro '${filtro}' fora dos storybooks. Use --storybooks para inclui-los.`
                : `nenhum item do catalogo casou com o filtro '${filtro}'`)

            process.exitCode = 1
            return
        }
    }

    const resultados = []

    // Sequencial de proposito, para nao esbarrar no rate limit da API do GitLab.
    for (const entrada of entradas) {
        if (!json) {
            process.stderr.write(`consultando ${entrada.chave}...\n`)
        }

        resultados.push(await coletar(cliente, entrada))
    }

    if (json) {
        console.log(JSON.stringify({
            geradoEm: new Date().toISOString(),
            sistemas: resultados,
            semEnderecoFixo: SEM_ENDERECO_FIXO
        }, null, 2))

        return
    }

    imprimirTexto(resultados)
}

main()
