import { avisarLoja } from "@/lib/loja/avisar-loja";
import { capitulosDoTexto, lerCapitulos, lerLinkDeVideo } from "@/lib/digitais/curso";
import { conferirLinkDeAcesso, lerConfigDigital } from "@/lib/digitais/produto";

import { ErroDigital, type ContextoDigital } from "../contexto";
import {
  atualizarResumoDoCurso,
  conferirArquivoDaBiblioteca,
  conferirLegendaDaBiblioteca,
  conferirVideoDaBiblioteca,
  exigirAula,
  exigirCurso,
  exigirModulo,
  garantirEntregavelDoCurso,
  gravarOrdem,
  lerEstruturaDoCurso,
  liberacaoDaAula,
  liberacaoDoModulo,
  textoLimpo,
} from "../cursos";
import { novaChaveDeEntrega } from "../entrega";
import { enderecoDaLoja, lerLojaDosDigitais } from "../loja";
import { COLUNAS_DO_PRODUTO, vendasDoProduto, type ProdutoDigitalNoBanco } from "../produtos";

/*
 * O que as rotas dos conteúdos em vídeo fazem (app/api/digitais/cursos/**),
 * fora delas: a tela do painel e o assistente (MCP) chamam as mesmas funções.
 * Toda mudança na estrutura refaz o resumo público e avisa a loja, como antes.
 */

const MAXIMO_DE_MODULOS = 100;
const MAXIMO_DE_AULAS = 500;
const MAXIMO_DE_MATERIAIS = 30;
const TIPOS = ["video", "texto", "materiais", "ao_vivo"] as const;
/* Duas horas: o tempo de conferir o curso como o aluno vê. */
const VALIDADE_DA_PREVIA_MS = 2 * 60 * 60 * 1000;

/* Os cursos da loja, com alunos, aulas e a conclusão média. */
export async function listarCursos({ admin, lojaId, pode }: ContextoDigital) {
  const { data, error } = await admin
    .from("products")
    .select(COLUNAS_DO_PRODUTO)
    .eq("user_id", lojaId)
    .eq("product_kind", "digital")
    .eq("digital_config->>tipo", "curso")
    .order("created_at", { ascending: false })
    .limit(300);
  if (error) throw new ErroDigital("Não foi possível ler os conteúdos em vídeo.", 500);
  const produtos = (data ?? []) as ProdutoDigitalNoBanco[];
  const ids = produtos.map((p) => p.id);

  const [acessos, progresso] = await Promise.all([
    ids.length
      ? admin
          .from("digital_entitlements")
          .select("product_id,client_id,status,expira_em,digital_deliverables!inner(tipo)")
          .eq("user_id", lojaId)
          .in("product_id", ids)
          .eq("digital_deliverables.tipo", "curso")
      : Promise.resolve({ data: [] }),
    ids.length
      ? admin.from("digital_course_progress").select("product_id,client_id,concluida_em").eq("user_id", lojaId).in("product_id", ids).not("concluida_em", "is", null)
      : Promise.resolve({ data: [] }),
  ]);

  const alunos = new Map<string, Set<string>>();
  for (const linha of (acessos.data ?? []) as Array<{ product_id: string; client_id: string; status: string; expira_em: string | null }>) {
    if (linha.status !== "ativo" || (linha.expira_em && new Date(linha.expira_em).getTime() < Date.now())) continue;
    const lista = alunos.get(linha.product_id) ?? new Set<string>();
    lista.add(linha.client_id);
    alunos.set(linha.product_id, lista);
  }
  const concluidas = new Map<string, number>();
  for (const linha of (progresso.data ?? []) as Array<{ product_id: string; client_id: string }>) {
    if (!alunos.get(linha.product_id)?.has(linha.client_id)) continue;
    concluidas.set(linha.product_id, (concluidas.get(linha.product_id) ?? 0) + 1);
  }

  return {
    podeEditar: pode("digital.products.write"),
    cursos: produtos.map((produto) => {
      const config = lerConfigDigital(produto.digital_config);
      const conteudo = config.curso?.conteudo ?? { modulos: [], aulas: 0, duracaoS: 0, materiais: 0 };
      const quantos = alunos.get(produto.id)?.size ?? 0;
      const vistas = concluidas.get(produto.id) ?? 0;
      return {
        id: produto.id,
        nome: produto.name,
        preco: Number(produto.price) || 0,
        imagem: produto.image_url,
        naLoja: Boolean(produto.show_in_catalog) && produto.active !== false,
        modulos: conteudo.modulos.length,
        aulas: conteudo.aulas,
        duracaoS: conteudo.duracaoS,
        alunos: quantos,
        /* Aulas concluídas sobre aulas possíveis dos alunos ativos. */
        conclusao: quantos && conteudo.aulas ? Math.min(100, Math.round((vistas / (quantos * conteudo.aulas)) * 100)) : 0,
      };
    }),
  };
}

/* O curso inteiro para o editor: a ficha, os módulos e aulas e os números. */
export async function lerCurso({ admin, lojaId, pode }: ContextoDigital, id: string, request: Request) {
  const { produto, config } = await exigirCurso(admin, lojaId, id);
  /* Curso criado antes do entregável (ou com ele apagado) volta a ter acesso. */
  if (pode("digital.products.write")) await garantirEntregavelDoCurso(admin, lojaId, id);
  const [modulos, vendas, loja, acessos] = await Promise.all([
    lerEstruturaDoCurso(admin, lojaId, id),
    vendasDoProduto(admin, id),
    lerLojaDosDigitais(admin, lojaId),
    admin
      .from("digital_entitlements")
      .select("client_id,status,expira_em,digital_deliverables!inner(tipo)")
      .eq("user_id", lojaId)
      .eq("product_id", id)
      .eq("digital_deliverables.tipo", "curso"),
  ]);
  const alunos = new Set(
    ((acessos.data ?? []) as Array<{ client_id: string; status: string; expira_em: string | null }>)
      .filter((a) => a.status === "ativo" && !(a.expira_em && new Date(a.expira_em).getTime() < Date.now()))
      .map((a) => a.client_id)
  ).size;
  const base = enderecoDaLoja(request, loja);
  return {
    podeEditar: pode("digital.products.write"),
    produto: {
      id: produto.id,
      nome: produto.name,
      descricao: produto.description ?? "",
      preco: Number(produto.price) || 0,
      imagem: produto.image_url,
      naLoja: Boolean(produto.show_in_catalog) && produto.active !== false,
      config,
      vendas,
      alunos,
      enderecoNaLoja: base ? `${base}/p/${produto.id}` : null,
    },
    modulos,
  };
}

/* --------------------------------------------------------------- módulos */

/* Um módulo novo, no fim do curso. */
export async function criarModulo({ admin, lojaId }: ContextoDigital, id: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  const corpo = (bruto ?? {}) as Record<string, unknown>;
  const { data: existentes } = await admin
    .from("digital_course_modules")
    .select("posicao")
    .eq("user_id", lojaId)
    .eq("product_id", id)
    .is("apagado_em", null);
  const lista = (existentes ?? []) as Array<{ posicao: number }>;
  if (lista.length >= MAXIMO_DE_MODULOS) throw new ErroDigital(`Um conteúdo em vídeo tem até ${MAXIMO_DE_MODULOS} módulos.`, 400);
  const titulo = textoLimpo(corpo.titulo, 160) ?? `Módulo ${lista.length + 1}`;
  const { data, error } = await admin
    .from("digital_course_modules")
    .insert({
      user_id: lojaId,
      product_id: id,
      titulo,
      descricao: textoLimpo(corpo.descricao, 1000),
      posicao: lista.reduce((maior, m) => Math.max(maior, m.posicao), -1) + 1,
      ...liberacaoDoModulo(corpo),
    })
    .select("id")
    .single();
  if (error || !data) throw new ErroDigital("Não foi possível criar o módulo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true, id: (data as { id: string }).id };
}

/* A ordem dos módulos. */
export async function ordenarModulos({ admin, lojaId }: ContextoDigital, id: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  const corpo = bruto as { ids?: unknown } | null;
  const ids = Array.isArray(corpo?.ids) ? corpo.ids.filter((v): v is string => typeof v === "string" && /^[0-9a-f-]{36}$/i.test(v)).slice(0, MAXIMO_DE_MODULOS) : [];
  if (!ids.length) throw new ErroDigital("Nada para ordenar.", 400);
  await gravarOrdem(admin, "digital_course_modules", lojaId, id, ids);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/* Muda o título, a descrição, quando o módulo abre ou a capa. */
export async function salvarModulo({ admin, lojaId }: ContextoDigital, id: string, moduloId: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  await exigirModulo(admin, lojaId, id, moduloId);
  const corpo = (bruto ?? {}) as Record<string, unknown>;
  const mudancas: Record<string, unknown> = { atualizado_em: new Date().toISOString() };
  if ("titulo" in corpo) {
    const titulo = textoLimpo(corpo.titulo, 160);
    if (!titulo) throw new ErroDigital("Dê um nome ao módulo.", 400);
    mudancas.titulo = titulo;
  }
  if ("descricao" in corpo) mudancas.descricao = textoLimpo(corpo.descricao, 1000);
  if ("liberacao" in corpo) Object.assign(mudancas, liberacaoDoModulo(corpo));
  /* A foto do pôster: só endereço https (o envio é para o depósito de imagens da loja). */
  if ("capa" in corpo) {
    const capa = typeof corpo.capa === "string" ? corpo.capa.trim() : "";
    if (capa && (!/^https:\/\/\S+$/.test(capa) || capa.length > 1000)) throw new ErroDigital("A capa precisa ser uma imagem enviada pela tela.", 400);
    mudancas.capa_url = capa || null;
  }
  const { error } = await admin.from("digital_course_modules").update(mudancas).eq("id", moduloId).eq("user_id", lojaId).eq("product_id", id);
  if (error) throw new ErroDigital("Não foi possível salvar o módulo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/*
 * Apaga o módulo e as aulas dele. O progresso e os comentários dos alunos
 * ficam guardados (o apagado só sai de vista), mas as aulas somem da área.
 */
export async function apagarModulo({ admin, lojaId }: ContextoDigital, id: string, moduloId: string, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  await exigirModulo(admin, lojaId, id, moduloId);
  const agora = new Date().toISOString();
  const { error: erroDasAulas } = await admin
    .from("digital_course_lessons")
    .update({ apagado_em: agora, atualizado_em: agora })
    .eq("module_id", moduloId)
    .eq("user_id", lojaId)
    .is("apagado_em", null);
  if (erroDasAulas) throw new ErroDigital("Não foi possível apagar os vídeos do módulo.", 500);
  const { error } = await admin
    .from("digital_course_modules")
    .update({ apagado_em: agora, atualizado_em: agora })
    .eq("id", moduloId)
    .eq("user_id", lojaId)
    .eq("product_id", id);
  if (error) throw new ErroDigital("Não foi possível apagar o módulo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/* ---------------------------------------------------------------- aulas */

/* Uma aula nova, no fim do módulo. */
export async function criarAula({ admin, lojaId }: ContextoDigital, id: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  const corpo = (bruto ?? {}) as Record<string, unknown>;
  const moduloId = typeof corpo.moduloId === "string" ? corpo.moduloId : "";
  await exigirModulo(admin, lojaId, id, moduloId);
  const { count } = await admin
    .from("digital_course_lessons")
    .select("id", { count: "exact", head: true })
    .eq("user_id", lojaId)
    .eq("product_id", id)
    .is("apagado_em", null);
  if ((count ?? 0) >= MAXIMO_DE_AULAS) throw new ErroDigital(`Um conteúdo em vídeo tem até ${MAXIMO_DE_AULAS} vídeos.`, 400);
  const { data: doModulo } = await admin
    .from("digital_course_lessons")
    .select("posicao")
    .eq("module_id", moduloId)
    .eq("user_id", lojaId)
    .is("apagado_em", null);
  const posicao = ((doModulo ?? []) as Array<{ posicao: number }>).reduce((maior, a) => Math.max(maior, a.posicao), -1) + 1;
  const tipo = TIPOS.includes(corpo.tipo as (typeof TIPOS)[number]) ? (corpo.tipo as (typeof TIPOS)[number]) : "video";
  const { data, error } = await admin
    .from("digital_course_lessons")
    .insert({
      user_id: lojaId,
      product_id: id,
      module_id: moduloId,
      titulo: textoLimpo(corpo.titulo, 160) ?? `Vídeo ${posicao + 1}`,
      tipo,
      posicao,
    })
    .select("id")
    .single();
  if (error || !data) throw new ErroDigital("Não foi possível criar o vídeo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true, id: (data as { id: string }).id };
}

/* A ordem das aulas de um módulo (a aula arrastada de outro módulo muda de módulo). */
export async function ordenarAulas({ admin, lojaId }: ContextoDigital, id: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  const corpo = bruto as { moduloId?: unknown; ids?: unknown } | null;
  const moduloId = typeof corpo?.moduloId === "string" ? corpo.moduloId : "";
  await exigirModulo(admin, lojaId, id, moduloId);
  const ids = Array.isArray(corpo?.ids) ? corpo.ids.filter((v): v is string => typeof v === "string" && /^[0-9a-f-]{36}$/i.test(v)).slice(0, MAXIMO_DE_AULAS) : [];
  if (!ids.length) throw new ErroDigital("Nada para ordenar.", 400);
  await gravarOrdem(admin, "digital_course_lessons", lojaId, id, ids, { module_id: moduloId });
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/* Salva a aula: tipo, vídeo (da biblioteca ou link), textos, quando abre... */
export async function salvarAula({ admin, lojaId }: ContextoDigital, id: string, aulaId: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  await exigirAula(admin, lojaId, id, aulaId);
  const corpo = (bruto ?? {}) as Record<string, unknown>;
  const mudancas: Record<string, unknown> = { atualizado_em: new Date().toISOString() };

  if ("titulo" in corpo) {
    const titulo = textoLimpo(corpo.titulo, 160);
    if (!titulo) throw new ErroDigital("Dê um nome ao vídeo.", 400);
    mudancas.titulo = titulo;
  }
  if ("tipo" in corpo) {
    if (!TIPOS.includes(corpo.tipo as (typeof TIPOS)[number])) throw new ErroDigital("Tipo de conteúdo inválido.", 400);
    mudancas.tipo = corpo.tipo;
  }
  if ("videoArquivoId" in corpo) {
    if (corpo.videoArquivoId === null) {
      mudancas.video_file_id = null;
    } else {
      const { arquivo, duracaoS } = await conferirVideoDaBiblioteca(admin, lojaId, String(corpo.videoArquivoId));
      mudancas.video_file_id = arquivo.id;
      mudancas.video_link = null;
      if (duracaoS && !("duracaoS" in corpo)) mudancas.duracao_s = duracaoS;
    }
  }
  if ("videoLink" in corpo) {
    if (corpo.videoLink === null || corpo.videoLink === "") {
      mudancas.video_link = null;
    } else {
      const link = lerLinkDeVideo(corpo.videoLink);
      if (!link) throw new ErroDigital("Esse link não é de um vídeo do YouTube, Vimeo ou Panda Video.", 400);
      mudancas.video_link = link;
      mudancas.video_file_id = null;
    }
  }
  if ("duracaoS" in corpo) {
    const duracao = corpo.duracaoS === null ? null : Math.round(Number(corpo.duracaoS));
    if (duracao !== null && !(duracao >= 0 && duracao <= 86400)) throw new ErroDigital("Confira a duração do vídeo.", 400);
    mudancas.duracao_s = duracao;
  }
  if ("descricao" in corpo) mudancas.descricao = textoLimpo(corpo.descricao, 4000);
  if ("texto" in corpo) mudancas.texto = textoLimpo(corpo.texto, 20000);
  if ("aoVivoEm" in corpo) {
    const quando = typeof corpo.aoVivoEm === "string" && corpo.aoVivoEm ? new Date(corpo.aoVivoEm) : null;
    if (quando && Number.isNaN(quando.getTime())) throw new ErroDigital("Confira o dia e a hora da transmissão ao vivo.", 400);
    mudancas.ao_vivo_em = quando ? quando.toISOString() : null;
  }
  if ("aoVivoLink" in corpo) {
    const link = textoLimpo(corpo.aoVivoLink, 2000);
    if (link && !/^https:\/\/[^\s]+$/i.test(link)) throw new ErroDigital("O link da transmissão ao vivo precisa começar com https://", 400);
    mudancas.ao_vivo_link = link;
  }
  /* A legenda (.vtt ou .srt da biblioteca) e os capítulos ("00:00 Abertura", um por linha). */
  if ("legendaArquivoId" in corpo) {
    mudancas.legenda_file_id =
      corpo.legendaArquivoId === null || corpo.legendaArquivoId === ""
        ? null
        : (await conferirLegendaDaBiblioteca(admin, lojaId, String(corpo.legendaArquivoId))).id;
  }
  if ("capitulos" in corpo) {
    mudancas.capitulos = typeof corpo.capitulos === "string" ? capitulosDoTexto(corpo.capitulos).capitulos : lerCapitulos(corpo.capitulos);
  }
  if ("previaGratis" in corpo) mudancas.previa_gratis = corpo.previaGratis === true;
  if ("comentarios" in corpo) mudancas.comentarios = corpo.comentarios !== false;
  if ("publicada" in corpo) mudancas.publicada = corpo.publicada !== false;
  if ("liberacao" in corpo) Object.assign(mudancas, liberacaoDaAula(corpo));

  const { error } = await admin.from("digital_course_lessons").update(mudancas).eq("id", aulaId).eq("user_id", lojaId).eq("product_id", id);
  if (error) throw new ErroDigital("Não foi possível salvar o vídeo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/* Apaga a aula (some da área dos alunos; o progresso e os comentários ficam guardados). */
export async function apagarAula({ admin, lojaId }: ContextoDigital, id: string, aulaId: string, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  await exigirAula(admin, lojaId, id, aulaId);
  const agora = new Date().toISOString();
  const { error } = await admin
    .from("digital_course_lessons")
    .update({ apagado_em: agora, atualizado_em: agora })
    .eq("id", aulaId)
    .eq("user_id", lojaId)
    .eq("product_id", id);
  if (error) throw new ErroDigital("Não foi possível apagar o vídeo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/* ------------------------------------------------------------ materiais */

/* Um material para a aula: um arquivo da biblioteca ou um link. */
export async function adicionarMaterial({ admin, lojaId }: ContextoDigital, id: string, aulaId: string, bruto: unknown, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  await exigirAula(admin, lojaId, id, aulaId);
  const corpo = (bruto ?? {}) as Record<string, unknown>;

  const { data: atuais } = await admin.from("digital_course_materials").select("posicao").eq("lesson_id", aulaId).eq("user_id", lojaId);
  const lista = (atuais ?? []) as Array<{ posicao: number }>;
  if (lista.length >= MAXIMO_DE_MATERIAIS) throw new ErroDigital(`Um vídeo tem até ${MAXIMO_DE_MATERIAIS} materiais.`, 400);
  const posicao = lista.reduce((maior, m) => Math.max(maior, m.posicao), -1) + 1;

  let linha: Record<string, unknown>;
  if (typeof corpo.arquivoId === "string") {
    const arquivo = await conferirArquivoDaBiblioteca(admin, lojaId, corpo.arquivoId);
    linha = { file_id: arquivo.id, titulo: textoLimpo(corpo.titulo, 160) ?? arquivo.nome.slice(0, 160) };
  } else {
    const link = conferirLinkDeAcesso(corpo.url);
    if (!link.ok) throw new ErroDigital(link.motivo, 400);
    linha = { url: link.url, titulo: textoLimpo(corpo.titulo, 160) ?? new URL(link.url).hostname };
  }
  const { data, error } = await admin
    .from("digital_course_materials")
    .insert({ user_id: lojaId, lesson_id: aulaId, posicao, ...linha })
    .select("id")
    .single();
  if (error || !data) throw new ErroDigital("Não foi possível pôr o material no vídeo.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true, id: (data as { id: string }).id };
}

/* Tira um material da aula (o arquivo continua na biblioteca). */
export async function tirarMaterial({ admin, lojaId }: ContextoDigital, id: string, aulaId: string, materialId: string, request: Request) {
  const { produto } = await exigirCurso(admin, lojaId, id);
  await exigirAula(admin, lojaId, id, aulaId);
  if (!/^[0-9a-f-]{36}$/i.test(materialId)) throw new ErroDigital("Material não encontrado.", 404);
  const { error } = await admin.from("digital_course_materials").delete().eq("id", materialId).eq("lesson_id", aulaId).eq("user_id", lojaId);
  if (error) throw new ErroDigital("Não foi possível tirar o material.", 500);
  await atualizarResumoDoCurso(admin, lojaId, id);
  if (produto.show_in_catalog) avisarLoja(lojaId, request);
  return { ok: true };
}

/* --------------------------------------------------------------- prévia */

/*
 * "Ver como aluno": um link de uso da equipe, de 2 horas, que abre os cursos
 * desta loja na área de membros sem compra (sem gravar progresso nem
 * comentar). A chave fica só em resumo no banco, como as outras.
 */
export async function abrirPreviaDoCurso({ admin, lojaId }: ContextoDigital, id: string, request: Request) {
  await exigirCurso(admin, lojaId, id);
  const loja = await lerLojaDosDigitais(admin, lojaId);
  const base = enderecoDaLoja(request, loja);
  if (!base) throw new ErroDigital("Sua loja ainda não tem endereço público.", 409);
  const { chave, resumo } = novaChaveDeEntrega();
  const { error } = await admin.from("digital_access_keys").insert({
    user_id: lojaId,
    client_id: null,
    order_id: null,
    catalog_order_id: null,
    finalidade: "entrar",
    hash: resumo,
    expira_em: new Date(Date.now() + VALIDADE_DA_PREVIA_MS).toISOString(),
  });
  if (error) throw new ErroDigital("Não foi possível abrir a prévia agora.", 500);
  return { ok: true, link: `${base}/membros/previa/${chave}?curso=${id}` };
}
