import { z } from "zod";
import { eq, and, or, like, isNotNull, isNull, desc, asc, lte, gte, sql } from "drizzle-orm";
import { router, publicProcedure, protectedProcedure, editorProcedure } from "./_core/trpc.js";
import { db } from "./_core/db.js";
import { users, bibliotecaLivros, bibliotecaConfig, bibliotecaHistorico, bibliotecaFavoritos, bibliotecaAnotacoes, bibliotecaMenuItens, bibliotecaAutores, bibliotecaLivroAutores, bibliotecaTemas, bibliotecaLivroTemas, bibliotecaColecoes, bibliotecaPaginas, bibliotecaHomeConfig, bibliotecaHeroCards, bibliotecaComoFunciona, bibliotecaAcervoItens, bibliotecaPublicoItens, bibliotecaBeneficios, bibliotecaFaqItens, bibliotecaAvaliacoes, bibliotecaAcessoRegistros, bibliotecaBanners, } from "../shared/schema.js";
import { gerarHashSenha, verificarSenha, emitirToken } from "./_core/auth.js";
import { definirCookieSessao, limparCookieSessao } from "./_core/cookies.js";
import { TRPCError } from "@trpc/server";
// Substitui todas as associações de autores/temas de um livro pelas
// novas — mais simples que tentar diferenciar o que mudou.
async function sincronizarAutores(livroId, autorIds) {
    if (autorIds === undefined)
        return;
    await db.delete(bibliotecaLivroAutores).where(eq(bibliotecaLivroAutores.livroId, livroId));
    if (autorIds.length > 0) {
        await db.insert(bibliotecaLivroAutores).values(autorIds.map((autorId) => ({ livroId, autorId })));
    }
}
async function sincronizarTemas(livroId, temaIds) {
    if (temaIds === undefined)
        return;
    await db.delete(bibliotecaLivroTemas).where(eq(bibliotecaLivroTemas.livroId, livroId));
    if (temaIds.length > 0) {
        await db.insert(bibliotecaLivroTemas).values(temaIds.map((temaId) => ({ livroId, temaId })));
    }
}
const authRouter = router({
    me: publicProcedure.query(async ({ ctx }) => {
        if (!ctx.usuario)
            return null;
        const [linha] = await db.select().from(users).where(eq(users.id, ctx.usuario.userId));
        if (!linha)
            return null;
        const { senhaHash: _senhaHash, ...usuarioPublico } = linha;
        return usuarioPublico;
    }),
    register: publicProcedure
        .input(z.object({ nome: z.string().min(2), email: z.string().email(), senha: z.string().min(8) }))
        .mutation(async ({ input, ctx }) => {
        const existente = await db.select().from(users).where(eq(users.email, input.email));
        if (existente.length > 0) {
            throw new TRPCError({ code: "CONFLICT", message: "Este e-mail já está cadastrado." });
        }
        const senhaHash = await gerarHashSenha(input.senha);
        const [resultado] = await db.insert(users).values({ nome: input.nome, email: input.email, senhaHash });
        const userId = resultado.insertId;
        const token = await emitirToken({ userId, papel: "user" });
        definirCookieSessao(ctx.res, token);
        return { id: userId, nome: input.nome, email: input.email };
    }),
    login: publicProcedure
        .input(z.object({ email: z.string().email(), senha: z.string() }))
        .mutation(async ({ input, ctx }) => {
        const [usuario] = await db.select().from(users).where(eq(users.email, input.email));
        if (!usuario || !(await verificarSenha(input.senha, usuario.senhaHash))) {
            throw new TRPCError({ code: "UNAUTHORIZED", message: "E-mail ou senha inválidos." });
        }
        const token = await emitirToken({ userId: usuario.id, papel: usuario.papel });
        definirCookieSessao(ctx.res, token);
        return { id: usuario.id, nome: usuario.nome, email: usuario.email };
    }),
    logout: publicProcedure.mutation(({ ctx }) => {
        limparCookieSessao(ctx.res);
        return { ok: true };
    }),
    atualizarPerfil: protectedProcedure
        .input(z.object({ nome: z.string().min(2) }))
        .mutation(async ({ input, ctx }) => {
        await db.update(users).set({ nome: input.nome }).where(eq(users.id, ctx.usuario.userId));
        return { ok: true };
    }),
    trocarSenha: protectedProcedure
        .input(z.object({ senhaAtual: z.string(), senhaNova: z.string().min(8) }))
        .mutation(async ({ input, ctx }) => {
        const [usuario] = await db.select().from(users).where(eq(users.id, ctx.usuario.userId));
        if (!usuario || !(await verificarSenha(input.senhaAtual, usuario.senhaHash))) {
            throw new TRPCError({ code: "UNAUTHORIZED", message: "Senha atual incorreta." });
        }
        const novoHash = await gerarHashSenha(input.senhaNova);
        await db.update(users).set({ senhaHash: novoHash }).where(eq(users.id, ctx.usuario.userId));
        return { ok: true };
    }),
});
const bibliotecaRouter = router({
    // Catálogo público — v1: todo livro publicado é visível (sem gating,
    // ver HUB-INTEGRATION-POINT em nivelAcesso no schema).
    getCatalogo: publicProcedure.query(async () => {
        const [homeConfig] = await db.select().from(bibliotecaHomeConfig).limit(1);
        const ordenacao = homeConfig?.todosLivrosOrdenacao ?? "recentes";
        const base = db.select().from(bibliotecaLivros).where(eq(bibliotecaLivros.status, "publicado"));
        if (ordenacao === "titulo")
            return base.orderBy(asc(bibliotecaLivros.titulo));
        if (ordenacao === "aleatorio")
            return base.orderBy(sql `RAND()`);
        return base.orderBy(desc(bibliotecaLivros.criadoEm));
    }),
    getLivro: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
        const [livro] = await db.select().from(bibliotecaLivros).where(eq(bibliotecaLivros.id, input.id));
        if (!livro)
            return null;
        const autores = await db
            .select({ id: bibliotecaAutores.id, nome: bibliotecaAutores.nome, papel: bibliotecaLivroAutores.papel })
            .from(bibliotecaLivroAutores)
            .innerJoin(bibliotecaAutores, eq(bibliotecaLivroAutores.autorId, bibliotecaAutores.id))
            .where(eq(bibliotecaLivroAutores.livroId, input.id));
        const temas = await db
            .select({ id: bibliotecaTemas.id, nome: bibliotecaTemas.nome })
            .from(bibliotecaLivroTemas)
            .innerJoin(bibliotecaTemas, eq(bibliotecaLivroTemas.temaId, bibliotecaTemas.id))
            .where(eq(bibliotecaLivroTemas.livroId, input.id));
        let colecao = null;
        if (livro.colecaoId) {
            [colecao] = await db.select().from(bibliotecaColecoes).where(eq(bibliotecaColecoes.id, livro.colecaoId));
        }
        const [resumoAvaliacao] = await db
            .select({ media: sql `AVG(${bibliotecaAvaliacoes.nota})`, total: sql `COUNT(*)` })
            .from(bibliotecaAvaliacoes)
            .where(eq(bibliotecaAvaliacoes.livroId, input.id));
        return {
            ...livro,
            autores,
            temas,
            colecao,
            avaliacaoMedia: resumoAvaliacao?.media ? Number(resumoAvaliacao.media) : null,
            avaliacaoTotal: Number(resumoAvaliacao?.total ?? 0),
        };
    }),
    // Alimenta o carrossel do hero. Critério configurável no CMS:
    // "manual" (marcados pelo editor, com fallback pros recentes),
    // "recentes" ou "aleatorio" — nesses dois últimos, não precisa
    // cadastrar livro, só o critério (pedido do Marsan).
    getDestaques: publicProcedure.query(async () => {
        const [homeConfig] = await db.select().from(bibliotecaHomeConfig).limit(1);
        const [config] = await db.select().from(bibliotecaConfig).limit(1);
        const qtd = config?.qtdSlidesHero ?? 5;
        const criterio = homeConfig?.destaquesCriterio ?? "manual";
        if (criterio === "recentes") {
            return db.select().from(bibliotecaLivros).where(eq(bibliotecaLivros.status, "publicado")).orderBy(desc(bibliotecaLivros.criadoEm)).limit(qtd);
        }
        if (criterio === "aleatorio") {
            return db.select().from(bibliotecaLivros).where(eq(bibliotecaLivros.status, "publicado")).orderBy(sql `RAND()`).limit(qtd);
        }
        const marcados = await db
            .select()
            .from(bibliotecaLivros)
            .where(and(eq(bibliotecaLivros.status, "publicado"), eq(bibliotecaLivros.destaque, true)))
            .orderBy(desc(bibliotecaLivros.atualizadoEm))
            .limit(qtd);
        if (marcados.length >= qtd)
            return marcados;
        const idsJaUsados = marcados.map((l) => l.id);
        const recentes = await db
            .select()
            .from(bibliotecaLivros)
            .where(eq(bibliotecaLivros.status, "publicado"))
            .orderBy(desc(bibliotecaLivros.criadoEm))
            .limit(qtd);
        const complemento = recentes.filter((l) => !idsJaUsados.includes(l.id)).slice(0, qtd - marcados.length);
        return [...marcados, ...complemento];
    }),
    buscar: publicProcedure.input(z.object({ q: z.string().min(1) })).query(async ({ input }) => {
        const termo = `%${input.q}%`;
        return db
            .select()
            .from(bibliotecaLivros)
            .where(and(eq(bibliotecaLivros.status, "publicado"), or(like(bibliotecaLivros.titulo, termo), like(bibliotecaLivros.autor, termo), like(bibliotecaLivros.serie, termo), like(bibliotecaLivros.resumo, termo))));
    }),
    getSerie: publicProcedure.input(z.object({ nome: z.string() })).query(async ({ input }) => {
        return db
            .select()
            .from(bibliotecaLivros)
            .where(and(eq(bibliotecaLivros.status, "publicado"), eq(bibliotecaLivros.serie, input.nome)))
            .orderBy(bibliotecaLivros.volumeNumero);
    }),
    // Lista de séries distintas — alimenta a seção "coleções" da Home.
    listarSeries: publicProcedure.query(async () => {
        const linhas = await db
            .selectDistinct({ serie: bibliotecaLivros.serie })
            .from(bibliotecaLivros)
            .where(and(eq(bibliotecaLivros.status, "publicado"), isNotNull(bibliotecaLivros.serie)));
        return linhas.map((l) => l.serie).filter((s) => !!s);
    }),
    // Filtro genérico por formato e/ou público-alvo — alimenta tanto a
    // página /explorar quanto as prateleiras de "Descubra" da Home.
    filtrar: publicProcedure
        .input(z.object({ formato: z.string().optional(), publicoAlvo: z.string().optional(), limite: z.number().max(40).optional() }))
        .query(async ({ input }) => {
        const condicoes = [eq(bibliotecaLivros.status, "publicado")];
        if (input.formato)
            condicoes.push(eq(bibliotecaLivros.formato, input.formato));
        if (input.publicoAlvo)
            condicoes.push(eq(bibliotecaLivros.publicoAlvo, input.publicoAlvo));
        return db
            .select()
            .from(bibliotecaLivros)
            .where(and(...condicoes))
            .orderBy(desc(bibliotecaLivros.criadoEm))
            .limit(input.limite ?? 20);
    }),
    getNovidades: publicProcedure.query(async () => {
        return db
            .select()
            .from(bibliotecaLivros)
            .where(eq(bibliotecaLivros.status, "publicado"))
            .orderBy(desc(bibliotecaLivros.criadoEm))
            .limit(10);
    }),
    admin: router({
        listar: editorProcedure.query(async () => {
            return db.select().from(bibliotecaLivros);
        }),
        criar: editorProcedure
            .input(z.object({
            titulo: z.string().min(1),
            subtitulo: z.string().optional(),
            serie: z.string().optional(),
            volumeNumero: z.number().optional(),
            autor: z.string().min(1),
            resumo: z.string().optional(),
            capaUrl: z.string().optional(),
            epubUrl: z.string().optional(),
            docxOriginalUrl: z.string().optional(),
            destaque: z.boolean().optional(),
            linkCompraImpresso: z.string().optional(),
            formato: z.enum(["texto", "ilustrado", "manga", "hq", "revista", "apostila", "audiolivro"]).optional(),
            publicoAlvo: z.enum(["primeiros_leitores", "infantil", "pre_adolescente", "adolescente", "jovem", "adulto"]).optional(),
            colecaoId: z.number().optional(),
            autorIds: z.array(z.number()).optional(),
            temaIds: z.array(z.number()).optional(),
        }))
            .mutation(async ({ input, ctx }) => {
            const { autorIds, temaIds, ...dadosLivro } = input;
            const [resultado] = await db.insert(bibliotecaLivros).values({
                ...dadosLivro,
                criadoPor: ctx.usuario.userId,
            });
            const livroId = resultado.insertId;
            await sincronizarAutores(livroId, autorIds);
            await sincronizarTemas(livroId, temaIds);
            return { id: livroId };
        }),
        atualizar: editorProcedure
            .input(z.object({
            id: z.number(),
            titulo: z.string().min(1).optional(),
            subtitulo: z.string().optional(),
            serie: z.string().optional(),
            volumeNumero: z.number().optional(),
            autor: z.string().min(1).optional(),
            resumo: z.string().optional(),
            capaUrl: z.string().optional(),
            epubUrl: z.string().optional(),
            docxOriginalUrl: z.string().optional(),
            status: z.enum(["rascunho", "publicado"]).optional(),
            destaque: z.boolean().optional(),
            linkCompraImpresso: z.string().optional(),
            formato: z.enum(["texto", "ilustrado", "manga", "hq", "revista", "apostila", "audiolivro"]).optional(),
            publicoAlvo: z.enum(["primeiros_leitores", "infantil", "pre_adolescente", "adolescente", "jovem", "adulto"]).optional(),
            colecaoId: z.number().optional(),
            autorIds: z.array(z.number()).optional(),
            temaIds: z.array(z.number()).optional(),
        }))
            .mutation(async ({ input }) => {
            const { id, autorIds, temaIds, ...dados } = input;
            if (Object.keys(dados).length > 0) {
                await db.update(bibliotecaLivros).set(dados).where(eq(bibliotecaLivros.id, id));
            }
            if (autorIds)
                await sincronizarAutores(id, autorIds);
            if (temaIds)
                await sincronizarTemas(id, temaIds);
            return { ok: true };
        }),
        excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
            await db.delete(bibliotecaLivroAutores).where(eq(bibliotecaLivroAutores.livroId, input.id));
            await db.delete(bibliotecaLivroTemas).where(eq(bibliotecaLivroTemas.livroId, input.id));
            await db.delete(bibliotecaPaginas).where(eq(bibliotecaPaginas.livroId, input.id));
            await db.delete(bibliotecaLivros).where(eq(bibliotecaLivros.id, input.id));
            return { ok: true };
        }),
    }),
    historico: router({
        // Posição salva de um livro específico — usado pra retomar de onde parou.
        obterPosicao: protectedProcedure.input(z.object({ livroId: z.number() })).query(async ({ input, ctx }) => {
            const [linha] = await db
                .select()
                .from(bibliotecaHistorico)
                .where(and(eq(bibliotecaHistorico.userId, ctx.usuario.userId), eq(bibliotecaHistorico.livroId, input.livroId)));
            return linha ?? null;
        }),
        registrar: protectedProcedure
            .input(z.object({ livroId: z.number(), posicaoLeitura: z.string() }))
            .mutation(async ({ input, ctx }) => {
            const [existente] = await db
                .select()
                .from(bibliotecaHistorico)
                .where(and(eq(bibliotecaHistorico.userId, ctx.usuario.userId), eq(bibliotecaHistorico.livroId, input.livroId)));
            if (existente) {
                await db
                    .update(bibliotecaHistorico)
                    .set({ posicaoLeitura: input.posicaoLeitura })
                    .where(eq(bibliotecaHistorico.id, existente.id));
            }
            else {
                await db.insert(bibliotecaHistorico).values({
                    userId: ctx.usuario.userId,
                    livroId: input.livroId,
                    posicaoLeitura: input.posicaoLeitura,
                });
            }
            return { ok: true };
        }),
        // "Continuar lendo" — últimos livros que o usuário abriu, mais recente primeiro.
        meuHistorico: protectedProcedure.query(async ({ ctx }) => {
            return db
                .select({
                livroId: bibliotecaHistorico.livroId,
                posicaoLeitura: bibliotecaHistorico.posicaoLeitura,
                atualizadoEm: bibliotecaHistorico.atualizadoEm,
                titulo: bibliotecaLivros.titulo,
                autor: bibliotecaLivros.autor,
                capaUrl: bibliotecaLivros.capaUrl,
                serie: bibliotecaLivros.serie,
            })
                .from(bibliotecaHistorico)
                .innerJoin(bibliotecaLivros, eq(bibliotecaHistorico.livroId, bibliotecaLivros.id))
                .where(eq(bibliotecaHistorico.userId, ctx.usuario.userId))
                .orderBy(desc(bibliotecaHistorico.atualizadoEm))
                .limit(10);
        }),
    }),
    favoritos: router({
        status: protectedProcedure.input(z.object({ livroId: z.number() })).query(async ({ input, ctx }) => {
            const [linha] = await db
                .select()
                .from(bibliotecaFavoritos)
                .where(and(eq(bibliotecaFavoritos.userId, ctx.usuario.userId), eq(bibliotecaFavoritos.livroId, input.livroId)));
            return { favorito: !!linha };
        }),
        alternar: protectedProcedure.input(z.object({ livroId: z.number() })).mutation(async ({ input, ctx }) => {
            const [existente] = await db
                .select()
                .from(bibliotecaFavoritos)
                .where(and(eq(bibliotecaFavoritos.userId, ctx.usuario.userId), eq(bibliotecaFavoritos.livroId, input.livroId)));
            if (existente) {
                await db.delete(bibliotecaFavoritos).where(eq(bibliotecaFavoritos.id, existente.id));
                return { favorito: false };
            }
            await db.insert(bibliotecaFavoritos).values({ userId: ctx.usuario.userId, livroId: input.livroId });
            return { favorito: true };
        }),
        meus: protectedProcedure.query(async ({ ctx }) => {
            return db
                .select({
                livroId: bibliotecaFavoritos.livroId,
                titulo: bibliotecaLivros.titulo,
                autor: bibliotecaLivros.autor,
                capaUrl: bibliotecaLivros.capaUrl,
            })
                .from(bibliotecaFavoritos)
                .innerJoin(bibliotecaLivros, eq(bibliotecaFavoritos.livroId, bibliotecaLivros.id))
                .where(eq(bibliotecaFavoritos.userId, ctx.usuario.userId))
                .orderBy(desc(bibliotecaFavoritos.criadoEm));
        }),
    }),
    // Marcador, grifo e compartilhamento são a mesma peça técnica de base
    // (CFI do epub.js) — por isso ficam unificados numa única tabela.
    anotacoes: router({
        marcadores: router({
            listar: protectedProcedure.input(z.object({ livroId: z.number() })).query(async ({ input, ctx }) => {
                return db
                    .select()
                    .from(bibliotecaAnotacoes)
                    .where(and(eq(bibliotecaAnotacoes.userId, ctx.usuario.userId), eq(bibliotecaAnotacoes.livroId, input.livroId), eq(bibliotecaAnotacoes.tipo, "marcador")))
                    .orderBy(desc(bibliotecaAnotacoes.criadoEm));
            }),
            criar: protectedProcedure
                .input(z.object({ livroId: z.number(), cfiInicio: z.string() }))
                .mutation(async ({ input, ctx }) => {
                const [resultado] = await db.insert(bibliotecaAnotacoes).values({
                    userId: ctx.usuario.userId,
                    livroId: input.livroId,
                    tipo: "marcador",
                    cfiInicio: input.cfiInicio,
                });
                return { id: resultado.insertId };
            }),
            remover: protectedProcedure.input(z.object({ id: z.number() })).mutation(async ({ input, ctx }) => {
                await db
                    .delete(bibliotecaAnotacoes)
                    .where(and(eq(bibliotecaAnotacoes.id, input.id), eq(bibliotecaAnotacoes.userId, ctx.usuario.userId)));
                return { ok: true };
            }),
        }),
        grifos: router({
            listar: protectedProcedure.input(z.object({ livroId: z.number() })).query(async ({ input, ctx }) => {
                return db
                    .select()
                    .from(bibliotecaAnotacoes)
                    .where(and(eq(bibliotecaAnotacoes.userId, ctx.usuario.userId), eq(bibliotecaAnotacoes.livroId, input.livroId), eq(bibliotecaAnotacoes.tipo, "grifo")));
            }),
            criar: protectedProcedure
                .input(z.object({
                livroId: z.number(),
                cfiInicio: z.string(),
                trechoTexto: z.string().max(2000),
                notaLeitor: z.string().max(500).optional(),
            }))
                .mutation(async ({ input, ctx }) => {
                const [resultado] = await db.insert(bibliotecaAnotacoes).values({
                    userId: ctx.usuario.userId,
                    livroId: input.livroId,
                    tipo: "grifo",
                    cfiInicio: input.cfiInicio,
                    trechoTexto: input.trechoTexto,
                    notaLeitor: input.notaLeitor,
                });
                return { id: resultado.insertId };
            }),
            remover: protectedProcedure.input(z.object({ id: z.number() })).mutation(async ({ input, ctx }) => {
                await db
                    .delete(bibliotecaAnotacoes)
                    .where(and(eq(bibliotecaAnotacoes.id, input.id), eq(bibliotecaAnotacoes.userId, ctx.usuario.userId)));
                return { ok: true };
            }),
        }),
        // Cria o registro compartilhável. O card de share usa a capa do livro
        // (já gerada na publicação, nunca gerada de novo aqui) + este trecho.
        // Limite de tamanho evita virar uma forma de tirar o livro inteiro
        // do app aos poucos.
        compartilhar: protectedProcedure
            .input(z.object({ livroId: z.number(), cfiInicio: z.string(), trechoTexto: z.string().min(1).max(600) }))
            .mutation(async ({ input, ctx }) => {
            const [resultado] = await db.insert(bibliotecaAnotacoes).values({
                userId: ctx.usuario.userId,
                livroId: input.livroId,
                tipo: "compartilhamento",
                cfiInicio: input.cfiInicio,
                trechoTexto: input.trechoTexto,
                publico: true,
            });
            return { id: resultado.insertId, url: `/share/trecho/${resultado.insertId}` };
        }),
    }),
});
export const appRouter = router({
    auth: authRouter,
    biblioteca: bibliotecaRouter,
    config: router({
        // Registro único (id=1). Se ainda não existir, cria com os padrões
        // (4 colunas desktop, 2 tablet, modo grade) na primeira leitura.
        obter: publicProcedure.query(async () => {
            let [linha] = await db.select().from(bibliotecaConfig).limit(1);
            if (!linha) {
                await db.insert(bibliotecaConfig).values({});
                [linha] = await db.select().from(bibliotecaConfig).limit(1);
            }
            return linha;
        }),
        atualizar: editorProcedure
            .input(z.object({
            colunasDesktop: z.number().int().min(1).max(6).optional(),
            colunasTablet: z.number().int().min(1).max(6).optional(),
            modoPadrao: z.enum(["grade", "lista"]).optional(),
            qtdSlidesHero: z.number().int().min(1).max(10).optional(),
        }))
            .mutation(async ({ input }) => {
            let [linha] = await db.select().from(bibliotecaConfig).limit(1);
            if (!linha) {
                await db.insert(bibliotecaConfig).values({});
                [linha] = await db.select().from(bibliotecaConfig).limit(1);
            }
            await db.update(bibliotecaConfig).set(input).where(eq(bibliotecaConfig.id, linha.id));
            return { ok: true };
        }),
    }),
    // Menu de navegação do topo — editável pelo editor, sem precisar de deploy.
    menu: router({
        listar: publicProcedure.query(async () => {
            return db.select().from(bibliotecaMenuItens).orderBy(asc(bibliotecaMenuItens.ordem));
        }),
        criar: editorProcedure
            .input(z.object({ rotulo: z.string().min(1), url: z.string().min(1), ordem: z.number().optional() }))
            .mutation(async ({ input }) => {
            const [resultado] = await db.insert(bibliotecaMenuItens).values(input);
            return { id: resultado.insertId };
        }),
        atualizar: editorProcedure
            .input(z.object({ id: z.number(), rotulo: z.string().min(1).optional(), url: z.string().min(1).optional(), ordem: z.number().optional() }))
            .mutation(async ({ input }) => {
            const { id, ...dados } = input;
            await db.update(bibliotecaMenuItens).set(dados).where(eq(bibliotecaMenuItens.id, id));
            return { ok: true };
        }),
        excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
            await db.delete(bibliotecaMenuItens).where(eq(bibliotecaMenuItens.id, input.id));
            return { ok: true };
        }),
    }),
    // Camada de colaboradores — substitui o antigo texto livre "autor".
    autores: router({
        listar: publicProcedure.query(async () => db.select().from(bibliotecaAutores).orderBy(asc(bibliotecaAutores.nome))),
        criar: editorProcedure
            .input(z.object({ nome: z.string().min(1), bio: z.string().optional() }))
            .mutation(async ({ input }) => {
            const [resultado] = await db.insert(bibliotecaAutores).values(input);
            return { id: resultado.insertId };
        }),
    }),
    // Camada 3 — temas (muitos-pra-muitos com livros).
    temas: router({
        listar: publicProcedure.query(async () => db.select().from(bibliotecaTemas).orderBy(asc(bibliotecaTemas.nome))),
        criar: editorProcedure.input(z.object({ nome: z.string().min(1) })).mutation(async ({ input }) => {
            const existente = await db.select().from(bibliotecaTemas).where(eq(bibliotecaTemas.nome, input.nome));
            if (existente.length > 0)
                return { id: existente[0].id };
            const [resultado] = await db.insert(bibliotecaTemas).values(input);
            return { id: resultado.insertId };
        }),
    }),
    // Camada 4 — coleções curadas (ex: "Heróis da Fé").
    colecoes: router({
        listar: publicProcedure.query(async () => db.select().from(bibliotecaColecoes).orderBy(asc(bibliotecaColecoes.nome))),
        obter: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
            const [colecao] = await db.select().from(bibliotecaColecoes).where(eq(bibliotecaColecoes.id, input.id));
            if (!colecao)
                return null;
            const livros = await db
                .select()
                .from(bibliotecaLivros)
                .where(and(eq(bibliotecaLivros.colecaoId, input.id), eq(bibliotecaLivros.status, "publicado")));
            return { ...colecao, livros };
        }),
        criar: editorProcedure
            .input(z.object({ nome: z.string().min(1), descricao: z.string().optional(), capaUrl: z.string().optional() }))
            .mutation(async ({ input }) => {
            const [resultado] = await db.insert(bibliotecaColecoes).values(input);
            return { id: resultado.insertId };
        }),
        atualizar: editorProcedure
            .input(z.object({ id: z.number(), nome: z.string().min(1).optional(), descricao: z.string().optional(), capaUrl: z.string().optional() }))
            .mutation(async ({ input }) => {
            const { id, ...dados } = input;
            await db.update(bibliotecaColecoes).set(dados).where(eq(bibliotecaColecoes.id, id));
            return { ok: true };
        }),
        excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
            await db.update(bibliotecaLivros).set({ colecaoId: null }).where(eq(bibliotecaLivros.colecaoId, input.id));
            await db.delete(bibliotecaColecoes).where(eq(bibliotecaColecoes.id, input.id));
            return { ok: true };
        }),
    }),
    // Pipeline de imagem — mangá/HQ/ilustrado. As imagens em si sobem via
    // REST (/api/admin/upload-paginas, igual ao padrão de upload-docx);
    // aqui só gravamos a ordem final depois do upload.
    paginas: router({
        listar: publicProcedure.input(z.object({ livroId: z.number() })).query(async ({ input }) => {
            return db.select().from(bibliotecaPaginas).where(eq(bibliotecaPaginas.livroId, input.livroId)).orderBy(asc(bibliotecaPaginas.ordem));
        }),
        salvar: editorProcedure
            .input(z.object({ livroId: z.number(), imagensUrl: z.array(z.string()) }))
            .mutation(async ({ input }) => {
            await db.delete(bibliotecaPaginas).where(eq(bibliotecaPaginas.livroId, input.livroId));
            if (input.imagensUrl.length > 0) {
                await db.insert(bibliotecaPaginas).values(input.imagensUrl.map((imagemUrl, indice) => ({ livroId: input.livroId, ordem: indice, imagemUrl })));
            }
            return { ok: true };
        }),
    }),
    // Avaliação por estrelas — um voto por pessoa por livro.
    avaliacoes: router({
        minha: protectedProcedure.input(z.object({ livroId: z.number() })).query(async ({ input, ctx }) => {
            const [linha] = await db
                .select()
                .from(bibliotecaAvaliacoes)
                .where(and(eq(bibliotecaAvaliacoes.livroId, input.livroId), eq(bibliotecaAvaliacoes.userId, ctx.usuario.userId)));
            return linha?.nota ?? null;
        }),
        avaliar: protectedProcedure
            .input(z.object({ livroId: z.number(), nota: z.number().min(1).max(5) }))
            .mutation(async ({ input, ctx }) => {
            const [existente] = await db
                .select()
                .from(bibliotecaAvaliacoes)
                .where(and(eq(bibliotecaAvaliacoes.livroId, input.livroId), eq(bibliotecaAvaliacoes.userId, ctx.usuario.userId)));
            if (existente) {
                await db.update(bibliotecaAvaliacoes).set({ nota: input.nota }).where(eq(bibliotecaAvaliacoes.id, existente.id));
            }
            else {
                await db.insert(bibliotecaAvaliacoes).values({ livroId: input.livroId, userId: ctx.usuario.userId, nota: input.nota });
            }
            return { ok: true };
        }),
    }),
    // Contador de acessos — dedução tipo YouTube: a mesma pessoa (ou o
    // mesmo IP, se anônima) só conta de novo depois de passar a janela
    // de tempo. Evita inflar só de atualizar a página repetidamente.
    registrarAcesso: publicProcedure
        .input(z.object({ livroId: z.number() }))
        .mutation(async ({ input, ctx }) => {
        const JANELA_HORAS = 4;
        const identificador = ctx.usuario ? `user:${ctx.usuario.userId}` : `ip:${ctx.ip}`;
        const desde = new Date(Date.now() - JANELA_HORAS * 60 * 60 * 1000);
        const [jaContou] = await db
            .select()
            .from(bibliotecaAcessoRegistros)
            .where(and(eq(bibliotecaAcessoRegistros.livroId, input.livroId), eq(bibliotecaAcessoRegistros.identificador, identificador), gte(bibliotecaAcessoRegistros.criadoEm, desde)));
        if (jaContou)
            return { contado: false };
        await db.insert(bibliotecaAcessoRegistros).values({ livroId: input.livroId, identificador });
        await db
            .update(bibliotecaLivros)
            .set({ contagemAcessos: sql `${bibliotecaLivros.contagemAcessos} + 1` })
            .where(eq(bibliotecaLivros.id, input.livroId));
        return { contado: true };
    }),
    // ═══════════════════════════════════════════════════════════
    // CMS DA HOME — cada seção é uma lista editável pelo admin.
    // ═══════════════════════════════════════════════════════════
    homeCms: router({
        config: router({
            obter: publicProcedure.query(async () => {
                let [linha] = await db.select().from(bibliotecaHomeConfig).limit(1);
                if (!linha) {
                    await db.insert(bibliotecaHomeConfig).values({});
                    [linha] = await db.select().from(bibliotecaHomeConfig).limit(1);
                }
                return linha;
            }),
            atualizar: editorProcedure
                .input(z.object({
                heroTitulo: z.string().optional(),
                heroTituloDestaque: z.string().optional(),
                heroSubtitulo: z.string().optional(),
                heroFundoUrl: z.string().optional(),
                heroOverlayIntensidade: z.number().min(0).max(100).optional(),
                heroBuscaAtiva: z.boolean().optional(),
                heroBuscaPlaceholder: z.string().optional(),
                destaquesCriterio: z.enum(["manual", "recentes", "aleatorio"]).optional(),
                todosLivrosOrdenacao: z.enum(["recentes", "aleatorio", "titulo"]).optional(),
                footerDescricao: z.string().optional(),
                footerCopyright: z.string().optional(),
                seoTitulo: z.string().optional(),
                seoDescricao: z.string().optional(),
                seoOgImagemUrl: z.string().optional(),
                seoKeywords: z.string().optional(),
                seoRobots: z.string().optional(),
                seoCanonical: z.string().optional(),
                corPrimaria: z.string().optional(),
                corFundo: z.string().optional(),
                secoesConfig: z.string().optional(),
            }))
                .mutation(async ({ input }) => {
                let [linha] = await db.select().from(bibliotecaHomeConfig).limit(1);
                if (!linha) {
                    await db.insert(bibliotecaHomeConfig).values({});
                    [linha] = await db.select().from(bibliotecaHomeConfig).limit(1);
                }
                await db.update(bibliotecaHomeConfig).set(input).where(eq(bibliotecaHomeConfig.id, linha.id));
                return { ok: true };
            }),
        }),
        heroCards: router({
            listar: publicProcedure.query(async () => db.select().from(bibliotecaHeroCards).where(eq(bibliotecaHeroCards.ativo, true)).orderBy(asc(bibliotecaHeroCards.ordem))),
            listarTodos: editorProcedure.query(async () => db.select().from(bibliotecaHeroCards).orderBy(asc(bibliotecaHeroCards.ordem))),
            criar: editorProcedure
                .input(z.object({ titulo: z.string().min(1), imagemUrl: z.string().optional(), livroId: z.number().optional(), linkUrl: z.string().optional(), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const [r] = await db.insert(bibliotecaHeroCards).values(input);
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({ id: z.number(), titulo: z.string().optional(), imagemUrl: z.string().optional(), livroId: z.number().optional(), linkUrl: z.string().optional(), ordem: z.number().optional(), ativo: z.boolean().optional() }))
                .mutation(async ({ input }) => {
                const { id, ...dados } = input;
                await db.update(bibliotecaHeroCards).set(dados).where(eq(bibliotecaHeroCards.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaHeroCards).where(eq(bibliotecaHeroCards.id, input.id));
                return { ok: true };
            }),
        }),
        comoFunciona: router({
            listar: publicProcedure.query(async () => db.select().from(bibliotecaComoFunciona).orderBy(asc(bibliotecaComoFunciona.ordem))),
            criar: editorProcedure
                .input(z.object({ icone: z.string(), numero: z.string(), titulo: z.string().min(1), descricao: z.string().optional(), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const [r] = await db.insert(bibliotecaComoFunciona).values(input);
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({ id: z.number(), icone: z.string().optional(), numero: z.string().optional(), titulo: z.string().optional(), descricao: z.string().optional(), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const { id, ...dados } = input;
                await db.update(bibliotecaComoFunciona).set(dados).where(eq(bibliotecaComoFunciona.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaComoFunciona).where(eq(bibliotecaComoFunciona.id, input.id));
                return { ok: true };
            }),
        }),
        acervoItens: router({
            listar: publicProcedure.query(async () => db.select().from(bibliotecaAcervoItens).where(eq(bibliotecaAcervoItens.ativo, true)).orderBy(asc(bibliotecaAcervoItens.ordem))),
            listarTodos: editorProcedure.query(async () => db.select().from(bibliotecaAcervoItens).orderBy(asc(bibliotecaAcervoItens.ordem))),
            criar: editorProcedure
                .input(z.object({ icone: z.string(), nome: z.string().min(1), descricao: z.string().optional(), imagemUrl: z.string().optional(), linkUrl: z.string().min(1), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const [r] = await db.insert(bibliotecaAcervoItens).values(input);
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({ id: z.number(), icone: z.string().optional(), nome: z.string().optional(), descricao: z.string().optional(), imagemUrl: z.string().optional(), linkUrl: z.string().optional(), ordem: z.number().optional(), ativo: z.boolean().optional() }))
                .mutation(async ({ input }) => {
                const { id, ...dados } = input;
                await db.update(bibliotecaAcervoItens).set(dados).where(eq(bibliotecaAcervoItens.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaAcervoItens).where(eq(bibliotecaAcervoItens.id, input.id));
                return { ok: true };
            }),
        }),
        publicoItens: router({
            listar: publicProcedure.query(async () => db.select().from(bibliotecaPublicoItens).where(eq(bibliotecaPublicoItens.ativo, true)).orderBy(asc(bibliotecaPublicoItens.ordem))),
            listarTodos: editorProcedure.query(async () => db.select().from(bibliotecaPublicoItens).orderBy(asc(bibliotecaPublicoItens.ordem))),
            criar: editorProcedure
                .input(z.object({ icone: z.string(), nome: z.string().min(1), faixaEtaria: z.string().min(1), linkUrl: z.string().min(1), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const [r] = await db.insert(bibliotecaPublicoItens).values(input);
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({ id: z.number(), icone: z.string().optional(), nome: z.string().optional(), faixaEtaria: z.string().optional(), linkUrl: z.string().optional(), ordem: z.number().optional(), ativo: z.boolean().optional() }))
                .mutation(async ({ input }) => {
                const { id, ...dados } = input;
                await db.update(bibliotecaPublicoItens).set(dados).where(eq(bibliotecaPublicoItens.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaPublicoItens).where(eq(bibliotecaPublicoItens.id, input.id));
                return { ok: true };
            }),
        }),
        beneficios: router({
            listar: publicProcedure.query(async () => db.select().from(bibliotecaBeneficios).where(eq(bibliotecaBeneficios.ativo, true)).orderBy(asc(bibliotecaBeneficios.ordem))),
            listarTodos: editorProcedure.query(async () => db.select().from(bibliotecaBeneficios).orderBy(asc(bibliotecaBeneficios.ordem))),
            criar: editorProcedure
                .input(z.object({ icone: z.string(), texto: z.string().min(1), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const [r] = await db.insert(bibliotecaBeneficios).values(input);
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({ id: z.number(), icone: z.string().optional(), texto: z.string().optional(), ordem: z.number().optional(), ativo: z.boolean().optional() }))
                .mutation(async ({ input }) => {
                const { id, ...dados } = input;
                await db.update(bibliotecaBeneficios).set(dados).where(eq(bibliotecaBeneficios.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaBeneficios).where(eq(bibliotecaBeneficios.id, input.id));
                return { ok: true };
            }),
        }),
        faqItens: router({
            listar: publicProcedure.query(async () => db.select().from(bibliotecaFaqItens).where(eq(bibliotecaFaqItens.ativo, true)).orderBy(asc(bibliotecaFaqItens.ordem))),
            listarTodos: editorProcedure.query(async () => db.select().from(bibliotecaFaqItens).orderBy(asc(bibliotecaFaqItens.ordem))),
            criar: editorProcedure
                .input(z.object({ pergunta: z.string().min(1), resposta: z.string().min(1), ordem: z.number().optional() }))
                .mutation(async ({ input }) => {
                const [r] = await db.insert(bibliotecaFaqItens).values(input);
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({ id: z.number(), pergunta: z.string().optional(), resposta: z.string().optional(), ordem: z.number().optional(), ativo: z.boolean().optional() }))
                .mutation(async ({ input }) => {
                const { id, ...dados } = input;
                await db.update(bibliotecaFaqItens).set(dados).where(eq(bibliotecaFaqItens.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaFaqItens).where(eq(bibliotecaFaqItens.id, input.id));
                return { ok: true };
            }),
        }),
        banners: router({
            // Só os ativos e dentro do período de exibição — a Home só consome isto.
            listarAtivos: publicProcedure.query(async () => {
                const hoje = new Date().toISOString().slice(0, 10);
                return db
                    .select()
                    .from(bibliotecaBanners)
                    .where(and(eq(bibliotecaBanners.ativo, true), or(isNull(bibliotecaBanners.dataInicio), lte(bibliotecaBanners.dataInicio, hoje)), or(isNull(bibliotecaBanners.dataFim), gte(bibliotecaBanners.dataFim, hoje))))
                    .orderBy(asc(bibliotecaBanners.ordem));
            }),
            listarTodos: editorProcedure.query(async () => db.select().from(bibliotecaBanners).orderBy(asc(bibliotecaBanners.ordem))),
            criar: editorProcedure
                .input(z.object({
                titulo: z.string().min(1),
                texto: z.string().optional(),
                imagemUrl: z.string().optional(),
                botaoTexto: z.string().optional(),
                linkUrl: z.string().min(1),
                dataInicio: z.string().optional(),
                dataFim: z.string().optional(),
                ordem: z.number().optional(),
            }))
                .mutation(async ({ input }) => {
                const { dataInicio, dataFim, ...resto } = input;
                const [r] = await db.insert(bibliotecaBanners).values({
                    ...resto,
                    dataInicio: dataInicio || undefined,
                    dataFim: dataFim || undefined,
                });
                return { id: r.insertId };
            }),
            atualizar: editorProcedure
                .input(z.object({
                id: z.number(),
                titulo: z.string().optional(),
                texto: z.string().optional(),
                imagemUrl: z.string().optional(),
                botaoTexto: z.string().optional(),
                linkUrl: z.string().optional(),
                dataInicio: z.string().optional(),
                dataFim: z.string().optional(),
                ordem: z.number().optional(),
                ativo: z.boolean().optional(),
            }))
                .mutation(async ({ input }) => {
                const { id, dataInicio, dataFim, ...resto } = input;
                await db
                    .update(bibliotecaBanners)
                    .set({
                    ...resto,
                    ...(dataInicio !== undefined && { dataInicio: dataInicio || null }),
                    ...(dataFim !== undefined && { dataFim: dataFim || null }),
                })
                    .where(eq(bibliotecaBanners.id, id));
                return { ok: true };
            }),
            excluir: editorProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
                await db.delete(bibliotecaBanners).where(eq(bibliotecaBanners.id, input.id));
                return { ok: true };
            }),
        }),
    }),
});
