import { requireSupportWrite } from "@/lib/impersonate/support";
/**
 * POST /api/v1/contacts/import — importa contatos de planilha CSV.
 *
 * Formato aceito: CSV RFC 4180 com linha de cabeçalho (delimitador detectado
 * automaticamente entre vírgula, ponto-e-vírgula e tabulação — Excel pt-BR
 * exporta ";"). Colunas reconhecidas e apelidos em `lib/contacts/csv.ts`.
 * XLSX é recusado na borda com instrução de exportar como CSV.
 *
 * Desfecho POR LINHA, nunca do lote inteiro:
 *   - linha inválida é pulada com motivo nominal (vem no response);
 *   - contato que já existe (telefone/e-mail/CPF da org) é contado como
 *     duplicado, não como erro;
 *   - as demais linhas seguem mesmo se uma falhar — uma planilha de 400 nomes
 *     não pode morrer inteira pelo telefone malformado da linha 7.
 *
 * Insert é linha a linha de propósito: `contacts` tem índices únicos parciais
 * (org+phone/org+email/org+cpf) e um insert em lote viraria tudo-ou-nada —
 * o 23505 de UM conflito descartaria as outras centenas criadas.
 */
import { randomUUID } from "node:crypto";
import { type NextRequest } from "next/server";

import { ok, fail } from "@/lib/api/wrappers";
import { lerCorpoComTeto } from "@/lib/api/corpo-com-teto";
import { requireRole } from "@/lib/auth/require-role";
import { audit } from "@/lib/audit";
import { camposCpfParaGravar } from "@/lib/contacts/cpf";
import { abrirConversaDoContatoNovo } from "@/lib/contacts/conversa-ao-criar";
import { traduzir } from "@/lib/i18n/dicionario";
import {
  CSV_MAX_BYTES,
  CSV_MAX_DATA_ROWS,
  decodificarCsv,
  mapHeader,
  mapLinha,
  parseCsv,
} from "@/lib/contacts/csv";
import { contactCreateSchemaDoPais } from "@/lib/schemas";
import { perfilDaOrganizacao } from "@/lib/legal/perfil-do-pais";
import { contatosDonosDosExtras } from "@/lib/channels/contato-por-telefone";
import { phoneLookupVariants } from "@/lib/channels/phone-variants";
import { createClient } from "@/lib/supabase/server";

export const dynamic = "force-dynamic";

const SOURCE_IMPORT_CSV = "import_csv";

interface LinhaErro {
  linha: number;
  motivo: string;
}

interface ImportSummary {
  total_linhas: number;
  imported: number;
  skipped_duplicates: number;
  errors: LinhaErro[];
}

export async function POST(req: NextRequest): Promise<Response> {
  const supportDenied = await requireSupportWrite();
  if (supportDenied) return supportDenied;

  const requestId = randomUUID();
  const supabase = await createClient();
  // spec 13 §4: escrita é agent+ (viewer é read-only), igual ao POST unitário.
  const authz = await requireRole("agent", { requestId, resource: "contacts" });
  if (!authz.ok) return authz.response;
  const user = authz.user;
  const orgId = authz.org.orgId;
  const t = (texto: string) => traduzir(texto, user.idioma);

  // O documento do titular, na régua do PAÍS da organização (issue #1033): o
  // cabeçalho aceito, a normalização do valor e a validação saem daqui. O país
  // vem da coluna da organização — nunca do arquivo enviado.
  const perfil = await perfilDaOrganizacao(supabase, orgId);
  const doc = perfil.documento;
  const schemaDoPais = contactCreateSchemaDoPais(perfil);

  // 64 KB de folga para o envelope multipart; o file.size abaixo é o check do CSV.
  const corpo = await lerCorpoComTeto(req, CSV_MAX_BYTES + 64 * 1024, {
    requestId,
    idioma: user.idioma,
    acimaDoTeto: t("Arquivo maior que ") + `${Math.floor(CSV_MAX_BYTES / 1024 / 1024)}MB.`,
  });
  if (!corpo.ok) return corpo.response;

  let file: File;
  try {
    const form = await corpo.corpo.formData();
    const f = form.get("file");
    if (!(f instanceof File)) throw new Error("sem arquivo");
    file = f;
  } catch {
    return fail(
      "validation_failed",
      t("Envie o arquivo como multipart/form-data no campo 'file'."),
      422,
      { requestId },
    );
  }

  const nome = file.name ?? "";
  const tipoOk =
    nome.toLowerCase().endsWith(".csv") ||
    file.type === "text/csv" ||
    file.type === "application/vnd.ms-excel";
  if (!tipoOk) {
    return fail(
      "validation_failed",
      t("Formato não suportado — envie um arquivo .csv. No Excel use 'Salvar como' → 'CSV UTF-8'."),
      422,
      { requestId },
    );
  }
  if (file.size > CSV_MAX_BYTES) {
    return fail(
      "validation_failed",
      t("Arquivo maior que ") + `${Math.floor(CSV_MAX_BYTES / 1024 / 1024)}MB.`,
      413,
      { requestId },
    );
  }

  // ─── Parse + validação de linhas (puro; nada tocou no banco ainda) ───────
  // Os BYTES, não `file.text()` — ver `decodificarCsv` (#483).
  const decodificado = decodificarCsv(await file.arrayBuffer());
  if ("erro" in decodificado) {
    return fail("validation_failed", t(decodificado.erro), 422, { requestId });
  }
  const text = decodificado.texto;
  const rows = parseCsv(text);
  if (rows.length < 2) {
    return fail("validation_failed", t("CSV vazio ou sem linhas de dados."), 422, { requestId });
  }
  const header = rows[0]!;
  const mapeado = mapHeader(header, t, doc);
  if (mapeado.motivo !== null) {
    return fail("validation_failed", `${t("Cabeçalho inválido:")} ${mapeado.motivo}.`, 422, {
      details: { header: header.join(", ") },
      requestId,
    });
  }
  const indices = mapeado.indices;

  const dataRows = rows.slice(1);
  if (dataRows.length > CSV_MAX_DATA_ROWS) {
    return fail(
      "validation_failed",
      `${t("Máximo de")} ${CSV_MAX_DATA_ROWS} ${t("linhas por importação — divida a planilha.")}`,
      422,
      { requestId },
    );
  }

  const candidatos: Array<{ linha: number; contato: Record<string, unknown> }> = [];
  const errors: LinhaErro[] = [];

  for (let i = 0; i < dataRows.length; i++) {
    const linha = i + 2; // 1-based contando o cabeçalho — bate com o editor de planilhas.
    const { contato, motivo } = mapLinha(dataRows[i]!, indices, t, doc, perfil.telefoneExemplo);
    if (motivo !== null) {
      errors.push({ linha, motivo });
      continue;
    }
    if (contato.cpf && !doc.valida(contato.cpf)) {
      // O texto sai do PERFIL: no Brasil continua "CPF inválido: …" (a mesma
      // chave do dicionário), e país sem checksum público diz na mensagem que a
      // conferência é de FORMA — prometer dígito verificado que não existe é o
      // que a issue #1033 proíbe.
      errors.push({ linha, motivo: t(doc.mensagemInvalido + ": ") + `"${contato.cpf}"` });
      continue;
    }
    const parsed = schemaDoPais.safeParse({ ...contato, source: SOURCE_IMPORT_CSV });
    if (!parsed.success) {
      const primeiro = parsed.error.issues[0];
      errors.push({ linha, motivo: primeiro?.message ?? t("dados inválidos") });
      continue;
    }
    candidatos.push({ linha, contato: parsed.data as Record<string, unknown> });
  }

  if (candidatos.length === 0) {
    const resumo: ImportSummary = {
      total_linhas: dataRows.length,
      imported: 0,
      skipped_duplicates: 0,
      errors,
    };
    return ok(resumo, { requestId });
  }

  // ─── Pré-filtro de duplicados contra o banco (uma query por identificador) ──
  //
  // Os índices únicos são parciais (excluem mesclados), então a
  // busca espelha o WHERE do índice — filtrar pelo que o índice não cobre
  // produziria "duplicado" imaginário.
  const phones = candidatos
    .map((c) => c.contato.phone_number as string | undefined)
    .filter((p): p is string => typeof p === "string");
  const emails = candidatos
    .map((c) => c.contato.email as string | undefined)
    .filter((e): e is string => typeof e === "string");

  const existentes = new Set<string>();
  if (phones.length > 0) {
    const lookup = [...new Set(phones.flatMap((p) => phoneLookupVariants(p)))];
    const { data } = await supabase
      .from("contacts")
      .select("phone_number")
      .eq("organization_id", orgId)
      .is("is_merged_into", null)
      .not("phone_number", "is", null)
      .in("phone_number", lookup);
    for (const r of data ?? []) {
      for (const v of phoneLookupVariants((r as { phone_number: string }).phone_number)) {
        existentes.add(`tel:${v}`);
      }
    }
    // Número que é EXTRA de uma ficha viva já é contato da org: conta como
    // duplicado aqui, em vez de chegar ao insert e cair no 23505 do banco.
    const donosDosExtras = await contatosDonosDosExtras(supabase, orgId, lookup);
    for (const numero of donosDosExtras.keys()) {
      for (const v of phoneLookupVariants(numero)) existentes.add(`tel:${v}`);
    }
  }
  if (emails.length > 0) {
    const { data } = await supabase
      .from("contacts")
      .select("email_normalized")
      .eq("organization_id", orgId)
      .is("is_merged_into", null)
      .in("email_normalized", emails.map((e) => e.toLowerCase()));
    for (const r of data ?? []) existentes.add(`email:${(r as { email_normalized: string }).email_normalized}`);
  }

  // ─── Insert linha a linha com desfecho individual ─────────────────────────
  let imported = 0;
  let skippedDuplicates = 0;

  for (const { linha, contato } of candidatos) {
    const phone = contato.phone_number as string | undefined;
    const email = contato.email as string | undefined;
    // Só contatos existentes ou gravados com sucesso reservam identificadores.
    // Cada repetição chega aqui para ter desfecho; uma linha inválida ou que
    // falhou no insert não pode descartar outra válida do mesmo arquivo.
    if (
      (phone && phoneLookupVariants(phone).some((v) => existentes.has(`tel:${v}`))) ||
      (email && existentes.has(`email:${email.toLowerCase()}`))
    ) {
      skippedDuplicates += 1;
      continue;
    }

    const insertRow: Record<string, unknown> = {
      organization_id: orgId,
      created_by_user_id: user.id,
      name: contato.name ?? null,
      display_name: contato.display_name ?? null,
      email: contato.email ?? null,
      phone_number: contato.phone_number ?? null,
      birthdate: contato.birthdate ?? null,
      tags: contato.tags ?? [],
      source: SOURCE_IMPORT_CSV,
      source_metadata: {},
      consent: {},
    };
    if (contato.cpf) {
      // LGPD: hash (dedupe) e versão cifrada nascem JUNTOS — o CHECK
      // `contacts_cpf_consistency` recusa a linha se só um dos dois existir
      // (#2522). Sem cifra disponível a linha segue SEM CPF, em vez de voltar
      // em `errors` e parar a reimportação inteira.
      Object.assign(insertRow, await camposCpfParaGravar(supabase, contato.cpf as string));
    }

    const { data: criado, error: insErr } = await supabase
      .from("contacts")
      .insert(insertRow)
      .select("id, display_name, phone_number")
      .single();

    if (insErr) {
      // Conflito de corrida com os índices únicos = duplicado, não erro.
      if (insErr.code === "23505") {
        skippedDuplicates += 1;
        continue;
      }
      errors.push({ linha, motivo: insErr.message });
      continue;
    }

    imported += 1;
    if (phone) existentes.add(`tel:${phone}`);
    if (email) existentes.add(`email:${email.toLowerCase()}`);

    // ── CRIAR CONTATO ABRE CONVERSA? AQUI, NÃO (#3718) ─────────────────────
    //
    // A criação unitária abre a conversa do contato novo; esta porta não abre, e
    // isso é REGRA, não esquecimento. A decisão é uma só e mora em
    // `lib/contacts/conversa-ao-criar.ts` — as duas portas chamam a MESMA
    // função e só declaram qual porta são; aqui o desfecho é `porta_nao_abre`.
    //
    // O preço, medido: o arquivo aceita até `CSV_MAX_DATA_ROWS` (500) contatos.
    // Abrir conversa para todos seria, por linha, `sessaoProntaParaEnvio` (1–2
    // SELECT em `channel_sessions`) + RPC `fn_service_begin` (advisory lock +
    // SELECT em `contacts` + SELECT … FOR NO KEY UPDATE em `conversations` +
    // SELECT em `channel_sessions` + INSERT em `conversations`) — ~2.500
    // statements neste request, que já faz 500 INSERTs e 500 `emit_event`. E o
    // que sobra não é o custo: é a caixa de entrada com 500 conversas `open`
    // que ninguém pediu e ninguém vai atender.
    //
    // A chamada é por linha de propósito (uma porta, um lugar para a regra), mas
    // ela responde ANTES de qualquer leitura: zero consulta ao banco por este
    // caminho, medido em `./route.test.ts`.
    await abrirConversaDoContatoNovo(supabase, {
      porta: "importacao_csv",
      organizationId: orgId,
      contactId: (criado as { id: string }).id,
      phoneNumber: phone ?? null,
      requestId,
    });

    // Mesmo evento do create unitário — quem consome `contact.created`
    // (workers, métricas) trata importado e manual igual.
    await supabase
      .rpc("emit_event", {
        p_event_type: "contact.created",
        p_entity_kind: "contact",
        p_entity_id: (criado as { id: string }).id,
        p_payload: {
          source: SOURCE_IMPORT_CSV,
          has_email: !!contato.email,
          has_phone: !!contato.phone_number,
          has_cpf: !!contato.cpf,
        },
        p_metadata: { request_id: requestId, actor_type: "user" },
        p_organization_id: orgId,
      })
      .then(({ error }) => {
        if (error) console.error("[contacts.import] emit_event failed", error.message);
      });
  }

  await audit({
    action: "contacts.imported",
    actorUserId: user.id,
    organizationId: orgId,
    resourceType: "contact",
    resourceId: null,
    requestId,
    metadata: {
      actor_type: "user",
      total_linhas: dataRows.length,
      imported,
      skipped_duplicates: skippedDuplicates,
      erros: errors.length,
    },
  });

  const resumo: ImportSummary = {
    total_linhas: dataRows.length,
    imported,
    skipped_duplicates: skippedDuplicates,
    errors,
  };
  return ok(resumo, { requestId });
}
