import { cookies, headers } from "next/headers";
import { NextResponse } from "next/server";
import { avisarConversa, canalDaConversa, canalDaLoja } from "@/lib/chats-aviso";
import { avisarLojaPorPush } from "@/lib/chats-push";
import { createAdminClient } from "@/lib/supabase-admin";
import { dentroDoExpediente, proximaAbertura } from "@/lib/expediente";
import { BUCKET_PRIVADO_DA_CONVERSA, TAMANHO_MAXIMO_DO_ANEXO, enderecoDoAnexo } from "@/lib/chats-servidor";

/*
  Conversa do visitante da loja.

  Quem esta olhando um produto e tem uma duvida nao deveria precisar de conta,
  nem sair da loja para o WhatsApp. A pessoa e reconhecida por uma chave
  aleatoria guardada no navegador dela — e tambem por um cookie, porque quando
  o site limpa o armazenamento a conversa inteira se perderia e ela teria que
  se apresentar de novo.

  Por isso o cuidado aqui e diferente do resto: a chave e a unica credencial, a
  conversa so existe se a loja tiver ligado o chat, e nada alem das mensagens
  daquela chave e devolvido.
*/

const LIMITE_DE_MENSAGENS = 60;
/* Quantas mensagens um visitante pode mandar por minuto antes de virar ruido. */
const LIMITE_POR_MINUTO = 12;
const TAMANHO_DA_MENSAGEM = 1000;
const COOKIE_DO_VISITANTE = "ktl_visitante";
const UM_ANO = 60 * 60 * 24 * 365;

function texto(valor: unknown) {
  return String(valor ?? "").trim();
}

function chaveValida(chave: string) {
  /* Formato de uuid: chave curta demais viraria adivinhacao. */
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(chave);
}

/* A chave vale se veio do navegador OU do cookie — o cookie e a rede de
   seguranca de quem limpou o armazenamento do site. */
async function chaveDaVez(informada: string) {
  if (chaveValida(informada)) return informada;
  const cookieStore = await cookies();
  const doCookie = texto(cookieStore.get(COOKIE_DO_VISITANTE)?.value);
  return chaveValida(doCookie) ? doCookie : "";
}

async function ipDoVisitante() {
  const cabecalhos = await headers();
  const encaminhado = texto(cabecalhos.get("x-forwarded-for")).split(",")[0].trim();
  return encaminhado || texto(cabecalhos.get("x-real-ip")) || null;
}

async function lojaDoSlug(admin: ReturnType<typeof createAdminClient>, slug: string) {
  if (!slug) return null;
  const { data } = await admin
    .from("profiles")
    .select(
      "id,slug,store_chat_enabled,store_chat_online,store_name,catalog_name,chat_business_hours,chat_away_message,chat_auto_offline"
    )
    .eq("slug", slug)
    .maybeSingle();
  const perfil = data as
    | {
        id: string;
        store_chat_enabled?: boolean;
        store_chat_online?: boolean;
        store_name?: string;
        catalog_name?: string;
        chat_business_hours?: unknown;
        chat_away_message?: string | null;
        chat_auto_offline?: boolean;
      }
    | null;
  if (!perfil?.id || perfil.store_chat_enabled !== true) return null;
  return perfil;
}

/*
  Online de verdade.

  A chave do balcão diz a intenção; o relógio diz a realidade. Com o expediente
  configurado e o desligamento automático ligado, fora do horário a loja aparece
  offline mesmo que alguém tenha esquecido a chave virada — porque prometer
  resposta imediata de madrugada é pior do que assumir que está fechado.
*/
function estaOnline(loja: {
  store_chat_online?: boolean;
  chat_auto_offline?: boolean;
  chat_business_hours?: unknown;
}) {
  if (loja.store_chat_online !== true) return false;
  if (loja.chat_auto_offline !== true) return true;
  return dentroDoExpediente(loja.chat_business_hours);
}

async function conversaDoVisitante(
  admin: ReturnType<typeof createAdminClient>,
  tenantId: string,
  visitorKey: string
) {
  const { data } = await admin
    .from("chat_threads")
    .select("id,visitor_name,visitor_contact,closed_at,sessions_count")
    .eq("kind", "visitor")
    .eq("user_id", tenantId)
    .eq("visitor_key", visitorKey)
    .maybeSingle();
  return (
    (data as {
      id: string;
      visitor_name: string | null;
      visitor_contact: string | null;
      closed_at: string | null;
      sessions_count: number | null;
    } | null) || null
  );
}

function comCookie(resposta: NextResponse, chave: string) {
  if (chaveValida(chave)) {
    resposta.cookies.set(COOKIE_DO_VISITANTE, chave, {
      maxAge: UM_ANO,
      sameSite: "lax",
      path: "/",
    });
  }
  return resposta;
}

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const slug = texto(searchParams.get("slug"));
  const visitorKey = await chaveDaVez(texto(searchParams.get("chave")));

  const admin = createAdminClient();
  const loja = await lojaDoSlug(admin, slug);
  if (!loja) return NextResponse.json({ ok: true, ligado: false, online: false, mensagens: [] });

  const online = estaOnline(loja);
  if (!visitorKey) return NextResponse.json({ ok: true, ligado: true, online, mensagens: [] });

  const conversa = await conversaDoVisitante(admin, loja.id, visitorKey);
  if (!conversa) {
    return comCookie(NextResponse.json({ ok: true, ligado: true, online, chave: visitorKey, mensagens: [] }), visitorKey);
  }

  const { data } = await admin
    .from("chat_messages")
    .select(
      "id,author_type,author_name,body,card,attachment_url,attachment_path,attachment_name,attachment_kind,created_at,deleted_at"
    )
    .eq("thread_id", conversa.id)
    /* Recado interno da equipe não é conversa do visitante. */
    .eq("internal", false)
    .order("created_at", { ascending: false })
    .limit(LIMITE_DE_MENSAGENS);

  const mensagens = ((data || []) as {
    id: string;
    author_type: string;
    author_name: string | null;
    body: string | null;
    card: Record<string, unknown> | null;
    attachment_url: string | null;
    attachment_path: string | null;
    attachment_name: string | null;
    attachment_kind: string | null;
    created_at: string;
    deleted_at: string | null;
  }[])
    .slice()
    .reverse()
    .map((mensagem) => ({
      id: mensagem.id,
      minha: mensagem.author_type === "client",
      quem: mensagem.author_type === "client" ? "Você" : mensagem.author_name || "Loja",
      texto: mensagem.deleted_at ? null : mensagem.body,
      anexoUrl: mensagem.deleted_at ? null : enderecoDoAnexo(mensagem),
      anexoNome: mensagem.deleted_at ? null : mensagem.attachment_name,
      anexoTipo: mensagem.deleted_at ? null : mensagem.attachment_kind,
      /* O produto ou o cupom que a loja mandou: chega como cartao, com foto e
         botao, e nao como link solto no meio da conversa. */
      cartao: mensagem.deleted_at ? null : mensagem.card,
      apagada: Boolean(mensagem.deleted_at),
      sistema: mensagem.author_type === "system",
      em: mensagem.created_at,
    }));

  /* O que a loja escreveu ficou lido no momento em que a pessoa abriu; e a
     passagem por aqui ja conta como visita. */
  await admin
    .from("chat_messages")
    .update({ read_by_other_at: new Date().toISOString() })
    .eq("thread_id", conversa.id)
    .in("author_type", ["store", "team", "system"])
    .is("read_by_other_at", null);

  await admin.from("chat_threads").update({ last_seen_at: new Date().toISOString() }).eq("id", conversa.id);

  return comCookie(
    NextResponse.json({
      ok: true,
      ligado: true,
      online,
      chave: visitorKey,
      threadId: conversa.id,
      nome: conversa.visitor_name,
      encerrada: Boolean(conversa.closed_at),
      mensagens,
    }),
    visitorKey
  );
}

export async function POST(request: Request) {
  /*
    A mensagem chega de dois jeitos.

    Só texto continua vindo como JSON, que é o caminho curto. Com arquivo vem
    como formulário — e arquivo aqui não é luxo: numa gráfica, a primeira coisa
    que o cliente quer mandar é a logo, a foto do que quer copiar, o modelo que
    viu. Sem isso a conversa acaba migrando para o WhatsApp na segunda frase.
  */
  const ehFormulario = String(request.headers.get("content-type") || "").includes("multipart/form-data");
  const form = ehFormulario ? await request.formData().catch(() => null) : null;
  const corpo = ehFormulario
    ? null
    : ((await request.json().catch(() => null)) as
        | { slug?: unknown; chave?: unknown; nome?: unknown; contato?: unknown; mensagem?: unknown }
        | null);

  const campo = (nome: string) => (form ? texto(form.get(nome)) : texto((corpo as Record<string, unknown>)?.[nome]));

  const slug = campo("slug");
  const visitorKey = await chaveDaVez(campo("chave"));
  const nome = campo("nome").slice(0, 80);
  const contato = campo("contato").slice(0, 80);
  const mensagem = campo("mensagem").slice(0, TAMANHO_DA_MENSAGEM);
  const arquivo = form?.get("arquivo");

  const admin = createAdminClient();
  const loja = await lojaDoSlug(admin, slug);
  if (!loja) return NextResponse.json({ error: "Esta loja não está atendendo por aqui." }, { status: 404 });
  if (!visitorKey) return NextResponse.json({ error: "Sessão inválida." }, { status: 400 });
  if (!mensagem && !arquivo) return NextResponse.json({ error: "Escreva uma mensagem." }, { status: 400 });
  if (!nome) return NextResponse.json({ error: "Diga seu nome para a loja saber com quem fala." }, { status: 400 });

  const agora = new Date().toISOString();
  const ip = await ipDoVisitante();
  const cabecalhos = await headers();
  const navegador = texto(cabecalhos.get("user-agent")).slice(0, 300) || null;

  type ConversaDoVisitante = NonNullable<Awaited<ReturnType<typeof conversaDoVisitante>>>;
  let conversa: ConversaDoVisitante | null = await conversaDoVisitante(admin, loja.id, visitorKey);
  let voltou = false;

  /*
    Freio de mao.

    Este endereco e publico: qualquer um pode chama-lo em looping e encher o
    sino da loja, o celular de quem atende e a tabela de mensagens. Ninguem
    escreve doze frases em um minuto de verdade — e quem escreve nao esta
    querendo comprar nada.
  */
  if (conversa) {
    const umMinutoAtras = new Date(Date.now() - 60 * 1000).toISOString();
    const { count } = await admin
      .from("chat_messages")
      .select("id", { count: "exact", head: true })
      .eq("thread_id", conversa.id)
      .eq("author_type", "client")
      .gte("created_at", umMinutoAtras);

    if (Number(count || 0) >= LIMITE_POR_MINUTO) {
      return NextResponse.json(
        { error: "Calma lá! Espere um instante antes de mandar outra mensagem." },
        { status: 429 }
      );
    }
  }

  if (!conversa) {
    const { data: criada, error } = await admin
      .from("chat_threads")
      .insert({
        user_id: loja.id,
        kind: "visitor",
        visitor_key: visitorKey,
        visitor_name: nome,
        visitor_contact: contato || null,
        visitor_ip: ip,
        visitor_user_agent: navegador,
        title: nome,
        first_seen_at: agora,
        last_seen_at: agora,
      })
      .select("id,visitor_name,visitor_contact,closed_at,sessions_count")
      .single();

    if (error || !criada) {
      conversa = await conversaDoVisitante(admin, loja.id, visitorKey);
      if (!conversa) return NextResponse.json({ error: error?.message || "Nao foi possivel abrir a conversa." }, { status: 400 });
    } else {
      conversa = criada as ConversaDoVisitante;
    }
  } else if (conversa.closed_at) {
    /* O atendimento tinha sido encerrado e a pessoa voltou: e a mesma conversa,
       mas uma visita nova — e isso e o que diz para a loja que ela insistiu. */
    voltou = true;
    await admin
      .from("chat_threads")
      .update({
        closed_at: null,
        closed_by: null,
        sessions_count: Number(conversa.sessions_count || 1) + 1,
        last_seen_at: agora,
        visitor_ip: ip,
        visitor_user_agent: navegador,
      })
      .eq("id", conversa.id);
  }

  /* O arquivo sobe depois de a conversa existir: e ela que dá o endereço da
     pasta, e sem conversa não há onde guardar. */
  let anexoUrl: string | null = null;
  let anexoNome: string | null = null;
  let anexoTipo: string | null = null;

  if (arquivo && typeof arquivo === "object" && "arrayBuffer" in arquivo) {
    const file = arquivo as File;
    if (file.size > TAMANHO_MAXIMO_DO_ANEXO) {
      return NextResponse.json({ error: "Arquivo maior que 8 MB." }, { status: 400 });
    }
    const extensao = (file.name.split(".").pop() || "bin").toLowerCase().replace(/[^a-z0-9]/g, "");
    const caminho = `${loja.id}/conversas/${conversa.id}/${crypto.randomUUID()}.${extensao}`;
    const { error: erroDoUpload } = await admin.storage
      .from(BUCKET_PRIVADO_DA_CONVERSA)
      .upload(caminho, file, { contentType: file.type || "application/octet-stream", upsert: false });
    if (erroDoUpload) return NextResponse.json({ error: erroDoUpload.message }, { status: 400 });

    /* Pasta privada: grava o caminho; o endereco sai assinado na hora de ver. */
    anexoUrl = caminho;
    anexoNome = file.name || "arquivo";
    anexoTipo = String(file.type || "").startsWith("image/") ? "image" : "file";
  }

  const { data: criada, error: erroDaMensagem } = await admin
    .from("chat_messages")
    .insert({
      thread_id: conversa.id,
      user_id: loja.id,
      author_type: "client",
      author_name: nome,
      body: mensagem || null,
      attachment_url: null,
      attachment_path: anexoUrl,
      attachment_name: anexoNome,
      attachment_kind: anexoTipo,
      read_by_other_at: agora,
    })
    .select("id,created_at")
    .single();

  if (erroDaMensagem) return NextResponse.json({ error: erroDaMensagem.message }, { status: 400 });

  const { data: atual } = await admin
    .from("chat_threads")
    .select("unread_store")
    .eq("id", conversa.id)
    .maybeSingle();

  await admin
    .from("chat_threads")
    .update({
      last_message_at: criada.created_at,
      last_message_preview: mensagem.slice(0, 120) || (anexoTipo === "image" ? "Imagem" : "Arquivo"),
      last_message_author: "client",
      updated_at: agora,
      last_seen_at: agora,
      unread_store: Number(atual?.unread_store || 0) + 1,
      visitor_name: nome,
      visitor_contact: contato || null,
    })
    .eq("id", conversa.id);

  /* A fala tambem e um passo da rota: e o que explica, na linha do tempo, por
     que a pessoa parou de navegar naquele ponto. */
  await admin.from("store_visitor_events").insert({
    user_id: loja.id,
    visitor_key: visitorKey,
    kind: "mensagem",
    title: mensagem.slice(0, 120),
    detail: `${nome} escreveu`,
    created_at: agora,
  });

  await avisarConversa([canalDaConversa(conversa.id), canalDaLoja(loja.id)], {
    threadId: conversa.id,
    autor: "client",
    autorNome: voltou ? `${nome} (voltou)` : nome,
    resumo: mensagem.slice(0, 120),
  });

  await avisarLojaPorPush(admin, {
    tenantId: loja.id,
    titulo: `${nome} · visitante da loja`,
    corpo: mensagem.slice(0, 120) || "Mandou um arquivo",
    rota: `/chats?conversa=${conversa.id}`,
    tag: `visitante-${conversa.id}`,
  });

  /*
    Recado de ausência.

    Fora do expediente, o silêncio é o pior atendimento possível: a pessoa fica
    olhando a tela sem saber se alguém vai responder hoje, amanhã ou nunca. Uma
    frase honesta — e com a hora em que a loja volta — vale mais do que a
    promessa vazia de "estamos online".

    Sai uma vez a cada seis horas: passar a conversa inteira repetindo o mesmo
    aviso é pior do que não avisar.
  */
  const recado = texto(loja.chat_away_message);
  if (!estaOnline(loja) && recado) {
    const seisHorasAtras = new Date(Date.now() - 6 * 60 * 60 * 1000).toISOString();
    const { count } = await admin
      .from("chat_messages")
      .select("id", { count: "exact", head: true })
      .eq("thread_id", conversa.id)
      .eq("author_type", "system")
      .gte("created_at", seisHorasAtras);

    if (!Number(count || 0)) {
      const volta = proximaAbertura(loja.chat_business_hours);
      const corpoDoRecado = volta ? `${recado}\n\nVoltamos ${volta}.` : recado;

      await admin.from("chat_messages").insert({
        thread_id: conversa.id,
        user_id: loja.id,
        author_type: "system",
        author_name: texto(loja.catalog_name) || texto(loja.store_name) || "Loja",
        body: corpoDoRecado,
        read_by_store_at: new Date().toISOString(),
      });

      await avisarConversa([canalDaConversa(conversa.id), `visitante-${visitorKey}`], {
        threadId: conversa.id,
        autor: "system",
        resumo: corpoDoRecado.slice(0, 120),
      });
    }
  }

  return comCookie(
    NextResponse.json({ ok: true, threadId: conversa.id, id: criada.id, chave: visitorKey }),
    visitorKey
  );
}
