import { createAdminClient } from "@/lib/supabase-admin";
import { esquecerLoja, lembrar, LEMBRANCA_DA_LOJA_S } from "@/lib/lembranca";
import { lerNavegacao, lerPaginas, type NavegacaoDaLoja, type PaginaDaLoja } from "@/lib/navegacao-da-loja";
import { comEstoqueV2 } from "@/lib/estoque-v2-da-loja.server";
import { ehFamiliaVendor } from "@/themes/registry";

/**
 * Os dados da vitrine de uma loja, lembrados por alguns minutos.
 *
 * A home e a categoria iam ao banco (em Oregon, 200 ms por ida) de três a
 * seis vezes em sequência a cada visita, e traziam os produtos com `*`:
 * descrição inteira em HTML e todas as opções de variação, que só a página
 * do produto usa. Numa loja de 216 produtos isso dava 2,6 MB de HTML.
 *
 * Aqui a loja inteira sai em duas idas (perfil, depois tudo em paralelo),
 * os produtos vêm enxutos (só o que card e filtro leem) e o resultado fica
 * lembrado por LEMBRANCA_DA_LOJA_S segundos na memória do processo. Quando o
 * lojista muda qualquer coisa da vitrine (produto, tema, banner, seção,
 * story, categoria, cupom), o admin avisa `/api/loja/revalidar` e a lembrança
 * some na hora; no pior caso, a mudança leva dois minutos para aparecer.
 *
 * O que depende do visitante (cliente logado, favoritos) fica fora daqui:
 * dentro da lembrança não pode haver cookie.
 */

/* O que os cards e os filtros dos dois templates leem. Nada de `*`. */
export const COLUNAS_DO_PRODUTO_NA_GRADE = [
  /* allow_negative_stock: com "Permitir estoque negativo" o card nao mostra
     "Esgotado" nem trava o "adicionar" (ver estoqueLimitaAVenda). */
  "id", "name", "description", "price", "image_url", "stock", "track_inventory", "allow_negative_stock", "user_id",
  "category_id", "unit_type", "price_on_request", "use_variation_price_only",
  /* O valor mínimo do item: o "adicionar" rápido do card leva para o carrinho. */
  "valor_minimo_item", "valor_minimo_loja",
  "min_sale_qty_enabled", "min_sale_qty", "max_sale_qty_enabled", "max_sale_qty",
  "allowed_sale_qty_enabled", "allowed_sale_quantities", "catalog_position", "created_at",
  /* product_kind: o card do produto digital leva o selo "Digital" e o botão
     dele abre a página do produto (a compra é rápida, fora do carrinho).
     digital_tipo: só o tipo do digital (o curso leva o selo "Curso"), sem
     trazer o resto do digital_config para a grade. digital_entrega e
     digital_pre_venda: o card diz "Sob encomenda" ou "Pré-venda" em vez de
     "Entrega na hora". */
  "product_kind", "digital_tipo:digital_config->>tipo",
  "digital_entrega:digital_config->entrega->>modo", "digital_pre_venda:digital_config->preVenda->>em",
].join(",");

export const SELECT_DA_GRADE =
  `${COLUNAS_DO_PRODUTO_NA_GRADE}, ` +
  "product_attributes(name,options,is_required,position,show_in_store), " +
  "product_price_tiers(min_qty,max_qty,unit_price,sort_order,active), " +
  "product_images(id,image_url,sort_order,is_primary)";

/* A busca do template padrão procura no nome e na descrição; 400 letras de
   texto puro bastam para isso e para a prévia do card em destaque. */
const LIMITE_DA_DESCRICAO = 400;

/* As linhas saem do Supabase sem tipo gerado neste app; `any` aqui é o mesmo
   que `select('*')` já devolvia às páginas. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Linha = any;

function textoCurto(valor: unknown, limite: number): string {
  const texto = String(valor ?? "")
    .replace(/<style[\s\S]*?<\/style>|<script[\s\S]*?<\/script>/gi, " ")
    .replace(/<[^>]+>/g, " ")
    .replace(/&nbsp;/gi, " ")
    .replace(/&amp;/gi, "&")
    .replace(/\s+/g, " ")
    .trim();
  return texto.length > limite ? `${texto.slice(0, limite).trimEnd()}…` : texto;
}

/**
 * O produto do jeito que a grade precisa: descrição curta em texto puro,
 * opções reduzidas ao preço (é só o que "a partir de" e o filtro usam),
 * galeria com as três primeiras fotos e `has_variations` já resolvido.
 * Mantém as chaves que os templates leem, então nada muda na tela.
 */
export function produtoEnxuto(produto: Linha): Linha {
  const atributos = Array.isArray(produto.product_attributes) ? (produto.product_attributes as Linha[]) : [];
  const atributosEnxutos = atributos
    .filter((atributo) => atributo && typeof atributo === "object")
    .map((atributo) => ({
      name: atributo.name ?? null,
      is_required: atributo.is_required === true,
      show_in_store: atributo.show_in_store !== false,
      position: typeof atributo.position === "number" ? atributo.position : null,
      options: (Array.isArray(atributo.options) ? atributo.options : []).map((opcao: Linha) =>
        typeof opcao === "string" ? "" : { price: Number((opcao as { price?: unknown } | null)?.price) || 0 }
      ),
    }));
  const imagens = (Array.isArray(produto.product_images) ? (produto.product_images as Linha[]) : [])
    .filter((imagem) => imagem && typeof imagem === "object" && imagem.image_url)
    .sort((a, b) => {
      const principal = Number(b.is_primary === true) - Number(a.is_primary === true);
      if (principal !== 0) return principal;
      return (Number(a.sort_order) || 0) - (Number(b.sort_order) || 0);
    })
    .slice(0, 3)
    .map((imagem) => ({
      id: imagem.id,
      image_url: imagem.image_url,
      sort_order: imagem.sort_order ?? null,
      is_primary: imagem.is_primary ?? null,
    }));
  return {
    ...produto,
    description: textoCurto(produto.description, LIMITE_DA_DESCRICAO),
    product_attributes: atributosEnxutos,
    product_images: imagens,
    has_variations: atributos.some((atributo) => atributo?.show_in_store !== false),
  };
}

/** As características de um produto como filtro: o nome ("Cor") e os rótulos
 *  das opções ("Azul", "Preta"). Só as visíveis na loja. */
export type CaracteristicaDoFiltro = { nome: string; rotulos: string[] };

/**
 * Os rótulos das opções que o `produtoEnxuto` tira (ele guarda só o preço de
 * cada opção, que é o que "a partir de" lê). A listagem da Boutique filtra
 * por Cor/Tamanho e precisa deles; fica num mapa À PARTE, por produto, para
 * que os outros temas continuem recebendo exatamente o mesmo produto enxuto
 * (a janela rápida do Padrão/Vitrine lê `options[].label` e mudaria).
 */
export function caracteristicasDoFiltro(produto: Linha): CaracteristicaDoFiltro[] {
  const atributos = Array.isArray(produto?.product_attributes) ? (produto.product_attributes as Linha[]) : [];
  return atributos
    .filter((atributo) => atributo && typeof atributo === "object" && atributo.show_in_store !== false)
    .map((atributo) => ({
      nome: String(atributo.name ?? "").trim(),
      rotulos: (Array.isArray(atributo.options) ? (atributo.options as Linha[]) : [])
        .map((opcao) => (typeof opcao === "string" ? opcao : String(opcao?.label ?? "")).trim())
        .filter(Boolean)
        .slice(0, 60),
    }))
    .filter((caracteristica) => caracteristica.nome && caracteristica.rotulos.length);
}

/** O pedaço do mapa das características que interessa a uma lista de produtos. */
export function caracteristicasDe(
  mapa: Record<string, CaracteristicaDoFiltro[]> | null | undefined,
  produtos: Array<{ id?: unknown }>
): Record<string, CaracteristicaDoFiltro[]> {
  const pedaco: Record<string, CaracteristicaDoFiltro[]> = {};
  if (!mapa) return pedaco;
  for (const produto of produtos) {
    const id = String(produto?.id ?? "");
    if (id && mapa[id]) pedaco[id] = mapa[id];
  }
  return pedaco;
}

export type DadosDaLoja = {
  profile: Linha;
  products: Linha[];
  categories: Linha[];
  sections: Linha[];
  /* Linhas de catalog_section_products com o produto (enxuto) embutido. */
  sectionProducts: Linha[];
  banners: Linha[];
  coupon: Linha | null;
  stories: Linha[];
  /* A página inicial foi montada pelo editor do painel (migration 196: há
     publicação em catalog_home_publicacoes). Só o Vendor depende disso: sem
     publicação ele monta a home sozinho, como sempre. */
  homeEditada: boolean;
  /* O cabeçalho, o menu e o rodapé publicados pelo editor (migration 197).
     null = a loja desenha como sempre desenhou. */
  navegacao: NavegacaoDaLoja | null;
  /* As páginas extras visíveis (/<slug>/pagina/<slug>), para o menu, o
     rodapé e o sitemap. */
  paginas: PaginaDaLoja[];
  /* Por id do produto: as características com os rótulos das opções (só o
     servidor lê; a Boutique passa à listagem só as dos produtos dela). */
  caracteristicas: Record<string, CaracteristicaDoFiltro[]>;
};

async function buscarDadosDaLoja(slug: string): Promise<DadosDaLoja | null> {
  /* A chave de servico, e nao a publica: a loja e lida pelo servidor, e o
     visitante deixa de precisar ler produto nenhum pela API (migration 160
     tira dele as colunas de preco e custo). As consultas abaixo filtram
     pelo dono, por ativo e por visivel, que e o que a regra publica fazia. */
  const supabase = createAdminClient();

  const { data: profile } = await supabase.from("profiles").select("*").eq("slug", slug).single();
  if (!profile) return null;
  const dono = String(profile.id);

  const consultaDosProdutosDasSecoes = supabase
    .from("catalog_section_products")
    .select(`*, section:catalog_sections!inner(id,user_id,active,pagina_id), product:products(${SELECT_DA_GRADE})`)
    .eq("section.user_id", dono)
    .eq("section.active", true)
    /* Só as seções da home: as das páginas extras (migration 197) têm
       pagina_id e são lidas pela página delas. */
    .is("section.pagina_id", null)
    .order("position", { ascending: true });

  const [produtos, categorias, secoes, banners, cupom, stories, produtosDasSecoes, publicacoes, navegacao, paginas] = await Promise.all([
    supabase
      .from("products")
      .select(SELECT_DA_GRADE)
      .eq("user_id", dono)
      .eq("active", true)
      .eq("show_in_catalog", true)
      .order("catalog_position", { ascending: true, nullsFirst: false })
      .order("created_at", { ascending: false }),
    supabase
      .from("categories")
      .select("id, name, slug, parent_id, is_visible")
      .eq("user_id", dono)
      .eq("is_visible", true)
      .order("name", { ascending: true }),
    supabase
      .from("catalog_sections")
      .select("*")
      .eq("user_id", dono)
      .eq("active", true)
      .is("pagina_id", null)
      .order("position", { ascending: true }),
    profile.catalog_slide_enabled
      ? supabase
          .from("catalog_banners")
          .select("id,image_url,link_type,product_id,link_url,open_in_new_tab,position,created_at")
          .eq("user_id", dono)
          .eq("type", "slide")
          .eq("active", true)
          .order("position", { ascending: true })
          .order("created_at", { ascending: true })
      : Promise.resolve({ data: [] as Linha[], error: null }),
    profile.catalog_announcement_coupon_id
      ? supabase
          .from("catalog_coupons")
          .select("id,code,type,value,active,starts_at,ends_at")
          .eq("id", profile.catalog_announcement_coupon_id)
          .eq("user_id", dono)
          .maybeSingle()
      : Promise.resolve({ data: null as Linha | null, error: null }),
    supabase
      .from("catalog_story_groups")
      .select(
        "id,title,thumbnail_url,position,items:catalog_story_items(id,image_url,product_id,position,product:products(id,name,price,price_on_request,image_url))"
      )
      .eq("user_id", dono)
      .eq("active", true)
      .order("position", { ascending: true }),
    consultaDosProdutosDasSecoes,
    /* Só a família Vendor (Vendor e Boutique) pergunta; os outros temas
       sempre mostraram as seções. Se a tabela não existir (migration 196 fora
       do banco), a resposta é erro e a home segue automática, como hoje. */
    ehFamiliaVendor(profile.catalog_theme as string | null)
      ? supabase
          .from("catalog_home_publicacoes")
          .select("id")
          .eq("user_id", dono)
          /* A home montada pelo editor: publicação da home que mexeu nas
             seções. Publicar só o menu, ou uma página extra, não troca a
             home automática do Vendor. */
          .is("pagina_id", null)
          .eq("secoes_mexidas", true)
          .limit(1)
      : Promise.resolve({ data: [] as Linha[], error: null }),
    supabase.from("catalog_navegacao").select("cabecalho,menu,rodape").eq("user_id", dono).maybeSingle(),
    supabase
      .from("catalog_paginas")
      .select("id,titulo,slug")
      .eq("user_id", dono)
      .eq("visivel", true)
      .order("position", { ascending: true })
      .order("created_at", { ascending: true })
      .limit(200),
  ]);

  /* Se a junção pela seção não for aceita (chave estrangeira com outro
     nome), cai no caminho antigo: as seções primeiro, os produtos depois. */
  let linhasDasSecoes: Linha[] = Array.isArray(produtosDasSecoes.data) ? (produtosDasSecoes.data as Linha[]) : [];
  if (produtosDasSecoes.error && Array.isArray(secoes.data) && secoes.data.length) {
    const alternativa = await supabase
      .from("catalog_section_products")
      .select(`*, product:products(${SELECT_DA_GRADE})`)
      .in("section_id", (secoes.data as Linha[]).map((secao) => String(secao.id)))
      .order("position", { ascending: true });
    linhasDasSecoes = Array.isArray(alternativa.data) ? (alternativa.data as Linha[]) : [];
  }

  /* Estoque v2: o selo "Esgotado" e o "esconder esgotados" seguem a conta do
     banco (composição ou item pronto). Fora do v2 a lista volta intacta. */
  const brutos = Array.isArray(produtos.data) ? (produtos.data as Linha[]) : [];
  const caracteristicas: Record<string, CaracteristicaDoFiltro[]> = {};
  for (const produto of brutos) {
    const lista = caracteristicasDoFiltro(produto);
    if (produto?.id && lista.length) caracteristicas[String(produto.id)] = lista;
  }
  const enxutos = brutos.map(produtoEnxuto);
  const enxutosDasSecoes = linhasDasSecoes.map((linha) =>
    linha.product && typeof linha.product === "object" ? produtoEnxuto(linha.product as Linha) : null
  );
  const marcados = await comEstoqueV2(supabase, dono, [...enxutos, ...enxutosDasSecoes]);
  const produtosDaGrade = marcados.slice(0, enxutos.length);
  const produtosDasSecoesNaGrade = marcados.slice(enxutos.length);

  return {
    profile: profile as Linha,
    products: produtosDaGrade,
    categories: Array.isArray(categorias.data) ? (categorias.data as Linha[]) : [],
    sections: Array.isArray(secoes.data) ? (secoes.data as Linha[]) : [],
    sectionProducts: linhasDasSecoes.map((linha, indice) => ({
      ...linha,
      section: undefined,
      product: produtosDasSecoesNaGrade[indice] ?? null,
    })),
    banners: Array.isArray(banners.data) ? (banners.data as Linha[]) : [],
    coupon: (cupom.data as Linha | null) ?? null,
    stories: Array.isArray(stories.data) ? (stories.data as Linha[]) : [],
    homeEditada: !publicacoes.error && Array.isArray(publicacoes.data) && publicacoes.data.length > 0,
    /* Erro de leitura (tabela fora do banco) = sem navegação própria: a
       loja segue com o desenho de sempre. */
    navegacao: navegacao.error ? null : lerNavegacao(navegacao.data, paginas.error ? [] : paginas.data),
    paginas: paginas.error ? [] : lerPaginas(paginas.data),
    caracteristicas,
  };
}

/* A lembrança em si (guardar, esperar a mesma promessa, esquecer no aviso)
   mora em lib/lembranca.ts, sem dependência nenhuma, para ter teste. */
export { lembrar, esquecerLoja, LEMBRANCA_DA_LOJA_S };

/**
 * A loja lida agora, sem passar pela lembrança (nem guardar nela): só o
 * editor da página inicial usa, para o lojista ver o que acabou de publicar.
 */
export function carregarLojaAgora(slug: string): Promise<DadosDaLoja | null> {
  return buscarDadosDaLoja(String(slug || "").trim().toLowerCase());
}

/** A loja lembrada: a mesma resposta para todas as páginas por dois minutos. */
export function carregarLoja(slug: string): Promise<DadosDaLoja | null> {
  const chave = String(slug || "").trim().toLowerCase();
  return lembrar(chave, () => buscarDadosDaLoja(chave));
}
