<template>
  <div class="eli-tabela">
    <EliTabelaDebug :isDev="isDev" :menuAberto="menuAberto" :menuPopupPos="menuPopupPos" />

    <EliTabelaEstados
      v-if="carregando || Boolean(erro) || !linhas.length"
      :carregando="carregando"
      :erro="erro"
      :mensagemVazio="tabela.mensagemVazio"
    />

    <template v-else>
      <EliTabelaCabecalho
        v-if="exibirBusca || temAcoesCabecalho"
        :exibirBusca="exibirBusca"
        :valorBusca="valorBusca"
        :acoesCabecalho="acoesCabecalho"
        @buscar="atualizarBusca"
        @colunas="abrirModalColunas"
      />

      <EliTabelaModalColunas
        :aberto="modalColunasAberto"
        :rotulosColunas="rotulosColunas"
        :configInicial="configColunas"
        @fechar="fecharModalColunas"
        @salvar="salvarModalColunas"
      />

      <table class="eli-tabela__table">
        <EliTabelaHead
          :colunas="colunasEfetivas"
          :temAcoes="temAcoes"
          :colunaOrdenacao="colunaOrdenacao"
          :direcaoOrdenacao="direcaoOrdenacao"
          @alternar-ordenacao="alternarOrdenacao"
        />

        <EliTabelaBody
          :colunas="colunasEfetivas"
          :linhas="linhas"
          :temAcoes="temAcoes"
          :menuAberto="menuAberto"
          :possuiAcoes="possuiAcoes"
          :toggleMenu="toggleMenu"
        />
      </table>

      <EliTabelaMenuAcoes
        ref="menuPopup"
        :menuAberto="menuAberto"
        :posicao="menuPopupPos"
        :acoes="menuAberto === null ? [] : acoesDisponiveisPorLinha(menuAberto)"
        :linha="menuAberto === null ? null : linhas[menuAberto]"
        @executar="({ acao, linha }) => { menuAberto = null; acao.acao(linha as never); }"
      />

      <EliTabelaPaginacao
        v-if="totalPaginas > 1 && quantidade > 0"
        :pagina="paginaAtual"
        :totalPaginas="totalPaginas"
        :maximoBotoes="tabela.maximo_botoes_paginacao"
        @alterar="irParaPagina"
      />
    </template>
  </div>
</template>

<script lang="ts">
/**
 * EliTabela
 * Componente de tabela consultável com busca, paginação, ordenação e ações por linha.
 */

/** Dependências do Vue (Composition API) */
import {
  computed,
  defineComponent,
  onBeforeUnmount,
  onMounted,
  PropType,
  ref,
  watch,
} from "vue";
/** Enum de códigos de resposta utilizado na consulta */
import { codigosResposta } from "p-respostas";
/** Componentes auxiliares */
import EliTabelaCabecalho from "./EliTabelaCabecalho.vue";
import EliTabelaEstados from "./EliTabelaEstados.vue";
import EliTabelaDebug from "./EliTabelaDebug.vue";
import EliTabelaHead from "./EliTabelaHead.vue";
import EliTabelaBody from "./EliTabelaBody.vue";
import EliTabelaMenuAcoes from "./EliTabelaMenuAcoes.vue";
import EliTabelaPaginacao from "./EliTabelaPaginacao.vue";
import EliTabelaModalColunas from "./EliTabelaModalColunas.vue";
/** Tipos da configuração/contrato da tabela */
import type { EliTabelaConsulta } from "./types-eli-tabela";
import {
  carregarConfigColunas,
  salvarConfigColunas,
  type EliTabelaColunasConfig,
} from "./colunasStorage";

export default defineComponent({
  name: "EliTabela",
  inheritAttrs: false,
  components: {
    EliTabelaCabecalho,
    EliTabelaEstados,
    EliTabelaDebug,
    EliTabelaHead,
    EliTabelaBody,
    EliTabelaMenuAcoes,
    EliTabelaPaginacao,
    EliTabelaModalColunas,
  },
  props: {
    /** Configuração principal da tabela (colunas, consulta e ações) */
    tabela: {
      type: Object as PropType<EliTabelaConsulta<any>>,
      required: true,
    },
  },
  setup(props) {
    /** Flag para habilitar elementos de debug */
    const isDev = import.meta.env.DEV;
    /** Estados de carregamento/erro e dados retornados */
    const carregando = ref(false);
    const erro = ref<string | null>(null);
    const linhas = ref<unknown[]>([]);
    const quantidade = ref<number>(0);

    /** Controle de visibilidade das ações por linha */
    const acoesVisiveis = ref<boolean[][]>([]);
    /** Estado do menu de ações (aberto, elemento e posição) */
    const menuAberto = ref<number | null>(null);
    // O componente EliTabelaMenuAcoes expõe `menuEl` (ref do elemento <ul>)
    const menuPopup = ref<{ menuEl: { value: HTMLElement | null } } | null>(null);
    const menuPopupPos = ref({ top: 0, left: 0 });

    /** Filtros e ordenação */
    const valorBusca = ref<string>("");
    const paginaAtual = ref(1);
    const colunaOrdenacao = ref<string | null>(null);
    const direcaoOrdenacao = ref<"asc" | "desc">("asc");

    /** Alias reativo da prop tabela */
    const tabela = computed(() => props.tabela);

    /** Exibição da busca e ações do cabeçalho */
    const exibirBusca = computed(() => Boolean(props.tabela.mostrarCaixaDeBusca));
    const acoesCabecalho = computed(() => props.tabela.acoesTabela ?? []);
    const temAcoesCabecalho = computed(() => acoesCabecalho.value.length > 0);

    /** Colunas: visibilidade/ordem com persistência */
    const modalColunasAberto = ref(false);
    const configColunas = ref<EliTabelaColunasConfig>(
      carregarConfigColunas(props.tabela.nome)
    );

    const rotulosColunas = computed(() => props.tabela.colunas.map((c) => c.rotulo));

    const colunasEfetivas = computed(() => {
      const colunas = props.tabela.colunas;
      const todosRotulos = rotulosColunas.value;
      const invisiveisSet = new Set(configColunas.value.invisiveis ?? []);

      // default: todas visiveis; so some se estiver explicitamente em invisiveis
      const visiveisBaseRotulos = todosRotulos.filter((r) => !invisiveisSet.has(r));
      const visiveisSet = new Set(visiveisBaseRotulos);

      // aplica ordem salva; novas (sem definicao) entram no fim, respeitando ordem original
      const ordemSalva = configColunas.value.visiveis ?? [];
      const ordemFinal: string[] = [];

      for (const r of ordemSalva) {
        if (visiveisSet.has(r)) ordemFinal.push(r);
      }
      for (const r of visiveisBaseRotulos) {
        if (!ordemFinal.includes(r)) ordemFinal.push(r);
      }

      // mapeia rótulo -> coluna, preservando duplicatas (se existirem) pelo primeiro match.
      // OBS: pressupoe rotulo unico; se repetir, comportamento fica indefinido.
      const mapa = new Map<string, any>();
      for (const c of colunas) {
        if (!mapa.has(c.rotulo)) mapa.set(c.rotulo, c);
      }

      return ordemFinal.map((r) => mapa.get(r)).filter(Boolean);
    });

    function abrirModalColunas() {
      modalColunasAberto.value = true;
    }

    function fecharModalColunas() {
      modalColunasAberto.value = false;
    }

    function salvarModalColunas(cfg: EliTabelaColunasConfig) {
      configColunas.value = cfg;
      salvarConfigColunas(props.tabela.nome, cfg);
      modalColunasAberto.value = false;
    }

    /** Registros por consulta (normaliza para inteiro positivo) */
    const registrosPorConsulta = computed(() => {
      const valor = props.tabela.registros_por_consulta;
      if (typeof valor === "number" && valor > 0) {
        return Math.floor(valor);
      }
      return 10;
    });

    /** Total de páginas calculado com base na quantidade */
    const totalPaginas = computed(() => {
      const limite = registrosPorConsulta.value;
      if (!limite || limite <= 0) return 1;

      const total = quantidade.value;
      if (!total) return 1;

      return Math.max(1, Math.ceil(total / limite));
    });

    /** Indica se existem ações por linha */
    const temAcoes = computed(() => (props.tabela.acoesLinha ?? []).length > 0);

    /** Sequencial para evitar race conditions entre consultas */
    let carregamentoSequencial = 0;

    /** Calcula a posição do menu de ações na viewport */
    function atualizarPosicaoMenu(anchor: HTMLElement) {
      const rect = anchor.getBoundingClientRect();
      const gap = 8;

      // Alinha no canto inferior direito do botão.
      // Se estourar a tela para baixo, abre para cima.
      const alturaMenu = menuPopup.value?.menuEl?.value?.offsetHeight ?? 0;
      const larguraMenu = menuPopup.value?.menuEl?.value?.offsetWidth ?? 180;

      let top = rect.bottom + gap;
      const left = rect.right - larguraMenu;

      if (alturaMenu && top + alturaMenu > window.innerHeight - gap) {
        top = rect.top - gap - alturaMenu;
      }

      menuPopupPos.value = {
        top: Math.max(gap, Math.round(top)),
        left: Math.max(gap, Math.round(left)),
      };
    }

    /** Fecha o menu quando ocorre clique fora */
    function handleClickFora(evento: MouseEvent) {
      if (menuAberto.value === null) return;

      const alvo = evento.target as Node;
      if (menuPopup.value?.menuEl?.value && menuPopup.value.menuEl.value.contains(alvo)) return;

      if (import.meta.env.DEV) {
        // eslint-disable-next-line no-console
        console.log("[EliTabela] click fora => fechar menu", { menuAberto: menuAberto.value });
      }

      menuAberto.value = null;
    }

    /** Alterna ordenação e recarrega os dados */
    function alternarOrdenacao(chave?: string) {
      if (!chave) return;

      if (colunaOrdenacao.value === chave) {
        direcaoOrdenacao.value = direcaoOrdenacao.value === "asc" ? "desc" : "asc";
        void carregar();
        return;
      }

      colunaOrdenacao.value = chave;
      direcaoOrdenacao.value = "asc";
      if (paginaAtual.value !== 1) {
        paginaAtual.value = 1;
      } else {
        void carregar();
      }
    }

    /** Atualiza a busca e reinicia paginação, se necessário */
    function atualizarBusca(texto: string) {
      if (valorBusca.value === texto) return;

      valorBusca.value = texto;
      if (paginaAtual.value !== 1) {
        paginaAtual.value = 1;
      } else {
        void carregar();
      }
    }

    /** Navega para a página solicitada com limites */
    function irParaPagina(pagina: number) {
      const alvo = Math.min(Math.max(1, pagina), totalPaginas.value);
      if (alvo !== paginaAtual.value) {
        paginaAtual.value = alvo;
      }
    }

    /**
     * Lista ações visíveis por linha, respeitando regras sync/async de `exibir`.
     */
    function acoesDisponiveisPorLinha(i: number) {
      const acoesLinha = props.tabela.acoesLinha ?? [];
      const visibilidade = acoesVisiveis.value[i] ?? [];

      return acoesLinha
        .map((acao, indice) => {
          const fallbackVisivel =
            acao.exibir === undefined
              ? true
              : typeof acao.exibir === "boolean"
              ? acao.exibir
              : false;

          return {
            acao,
            indice,
            visivel: visibilidade[indice] ?? fallbackVisivel,
          };
        })
        .filter((item) => item.visivel);
    }

    /** Informa se a linha possui ações disponíveis */
    function possuiAcoes(i: number) {
      return acoesDisponiveisPorLinha(i).length > 0;
    }

    /** Abre/fecha o menu de ações da linha e posiciona o popup */
    function toggleMenu(i: number, evento?: MouseEvent) {
      if (!possuiAcoes(i)) return;

      if (import.meta.env.DEV) {
        // eslint-disable-next-line no-console
        console.log("[EliTabela] toggleMenu (antes)", { i, atual: menuAberto.value });
      }

      if (menuAberto.value === i) {
        menuAberto.value = null;

        if (import.meta.env.DEV) {
          // eslint-disable-next-line no-console
          console.log("[EliTabela] toggleMenu => fechou", { i });
        }
        return;
      }

      menuAberto.value = i;

      if (import.meta.env.DEV) {
        // eslint-disable-next-line no-console
        console.log("[EliTabela] toggleMenu => abriu", { i });
      }

      // posiciona assim que abrir
      const anchor = (evento?.currentTarget as HTMLElement | null) ?? null;
      if (anchor) {
        // primeiro posicionamento (antes do menu medir)
        atualizarPosicaoMenu(anchor);
        // reposiciona no próximo frame para pegar altura real
        requestAnimationFrame(() => atualizarPosicaoMenu(anchor));
      }
    }

    /**
     * Executa consulta, aplica filtros e resolve visibilidade de ações.
     * Usa o contador sequencial para ignorar respostas antigas.
     */
    async function carregar() {
      const idCarregamento = ++carregamentoSequencial;
      carregando.value = true;
      erro.value = null;
      acoesVisiveis.value = [];
      menuAberto.value = null;

      const limite = Math.max(1, registrosPorConsulta.value);
      const offset = (paginaAtual.value - 1) * limite;

      const parametrosConsulta: {
        coluna_ordem?: never;
        direcao_ordem?: "asc" | "desc";
        offSet: number;
        limit: number;
        texto_busca?: string;
      } = {
        offSet: offset,
        limit: limite,
      };

      if (valorBusca.value) {
        parametrosConsulta.texto_busca = valorBusca.value;
      }

      if (colunaOrdenacao.value) {
        parametrosConsulta.coluna_ordem = colunaOrdenacao.value as never;
        parametrosConsulta.direcao_ordem = direcaoOrdenacao.value;
      }

      try {
        const tabelaConfig = props.tabela;
        const res = await tabelaConfig.consulta(parametrosConsulta);

        if (idCarregamento !== carregamentoSequencial) return;

        if (res.cod !== codigosResposta.sucesso) {
          linhas.value = [];
          quantidade.value = 0;
          erro.value = res.mensagem;
          return;
        }

        const valores = res.valor?.valores ?? [];
        const total = res.valor?.quantidade ?? valores.length;

        linhas.value = valores;
        quantidade.value = total;

        const totalPaginasRecalculado = Math.max(1, Math.ceil((total || 0) / limite));
        if (paginaAtual.value > totalPaginasRecalculado) {
          paginaAtual.value = totalPaginasRecalculado;
          return;
        }

        const acoesLinhaConfiguradas = tabelaConfig.acoesLinha ?? [];
        if (!acoesLinhaConfiguradas.length) {
          acoesVisiveis.value = [];
          return;
        }

        const preResultado = valores.map(() =>
          acoesLinhaConfiguradas.map((acao) => {
            if (acao.exibir === undefined) return true;
            if (typeof acao.exibir === "boolean") return acao.exibir;
            return false;
          })
        );

        acoesVisiveis.value = preResultado;

        const visibilidade = await Promise.all(
          valores.map(async (linha) =>
            Promise.all(
              acoesLinhaConfiguradas.map(async (acao) => {
                if (acao.exibir === undefined) return true;
                if (typeof acao.exibir === "boolean") return acao.exibir;

                try {
                  const resultado = acao.exibir(linha as never);
                  return Boolean(await Promise.resolve(resultado));
                } catch {
                  return false;
                }
              })
            )
          )
        );

        if (idCarregamento === carregamentoSequencial) {
          acoesVisiveis.value = visibilidade;
        }
      } catch (e) {
        if (idCarregamento !== carregamentoSequencial) return;

        linhas.value = [];
        quantidade.value = 0;
        erro.value = e instanceof Error ? e.message : "Erro ao carregar dados.";
      } finally {
        if (idCarregamento === carregamentoSequencial) {
          carregando.value = false;
        }
      }
    }

    /** Ciclo de vida: registra listener global e carrega dados iniciais */
    onMounted(() => {
      document.addEventListener("click", handleClickFora);
      void carregar();
    });

    /** Ciclo de vida: remove listener global */
    onBeforeUnmount(() => {
      document.removeEventListener("click", handleClickFora);
    });

    /** Watch: ao desabilitar busca, limpa termo e recarrega */
    watch(
      () => props.tabela.mostrarCaixaDeBusca,
      (mostrar) => {
        if (!mostrar && valorBusca.value) {
          valorBusca.value = "";
          if (paginaAtual.value !== 1) {
            paginaAtual.value = 1;
          } else {
            void carregar();
          }
        }
      }
    );

    /** Watch: mudança de página dispara nova consulta */
    watch(paginaAtual, (nova, antiga) => {
      if (nova !== antiga) void carregar();
    });

    /** Watch: troca de configuração reseta estados e recarrega */
    watch(
      () => props.tabela,
      () => {
        menuAberto.value = null;
        colunaOrdenacao.value = null;
        direcaoOrdenacao.value = "asc";
        valorBusca.value = "";
        modalColunasAberto.value = false;
        configColunas.value = carregarConfigColunas(props.tabela.nome);
        if (paginaAtual.value !== 1) {
          paginaAtual.value = 1;
        } else {
          void carregar();
        }
      }
    );

    /** Watch: alteração do limite de registros reinicia paginação */
    watch(
      () => props.tabela.registros_por_consulta,
      () => {
        if (paginaAtual.value !== 1) {
          paginaAtual.value = 1;
        } else {
          void carregar();
        }
      }
    );

    /** Watch: mudança nas linhas fecha o menu aberto */
    watch(linhas, () => {
      menuAberto.value = null;
    });

    /** Exposição para o template (state, computed, helpers e actions) */
    return {
      // state
      isDev,
      tabela,
      carregando,
      erro,
      linhas,
      quantidade,
      menuAberto,
      valorBusca,
      paginaAtual,
      colunaOrdenacao,
      direcaoOrdenacao,
      totalPaginas,

      // computed
      exibirBusca,
      acoesCabecalho,
      temAcoesCabecalho,
      temAcoes,
      colunasEfetivas,
      rotulosColunas,
      modalColunasAberto,
      configColunas,

      abrirModalColunas,
      fecharModalColunas,
      salvarModalColunas,

      // actions
      alternarOrdenacao,
      atualizarBusca,
      irParaPagina,
      acoesDisponiveisPorLinha,
      possuiAcoes,
      toggleMenu,

      // popup
      menuPopup,
      menuPopupPos,
    };
  },
});
</script>

<style src="./EliTabela.css"></style>
