import { NextResponse } from "next/server";
import { createAdminClient, createClient } from "@/lib/supabase-server";
import { canCompany, canReadSupplierLookup, getCompanyContext } from "@/lib/company-context";
import { conferirFornecedor, tipoDoDocumento } from "@/lib/fornecedores/documento";

type SupplierStatus = "ativo" | "pendente" | "inativo";

type SupplierPayload = {
  name?: unknown;
  /* `cnpj` continua sendo o nome do campo no corpo por compatibilidade com o
     que ja esta no ar; `document` e o nome novo. Os dois chegam aqui como o
     documento inteiro, CNPJ ou CPF. */
  cnpj?: unknown;
  document?: unknown;
  category?: unknown;
  city?: unknown;
  email?: unknown;
  phone?: unknown;
  paymentTermsPrimary?: unknown;
  paymentTermsSecondary?: unknown;
  status?: unknown;
  notes?: unknown;
};

function documentoDoCorpo(raw: SupplierPayload) {
  const declarado = normalizeString(raw.document);
  return digits(declarado || raw.cnpj);
}

function normalizeString(value: unknown) {
  return typeof value === "string" ? value.trim() : "";
}

function digits(value: unknown) {
  return String(value || "").replace(/\D/g, "");
}

function normalizeStatus(value: unknown): SupplierStatus {
  const cleaned = normalizeString(value).toLowerCase();
  if (cleaned === "pendente") return "pendente";
  if (cleaned === "inativo") return "inativo";
  return "ativo";
}

async function getAuth(requiredPermission: "suppliers.read" | "suppliers.list.write") {
  const supabase = await createClient();
  const {
    data: { user },
    error,
  } = await supabase.auth.getUser();

  if (error || !user) return { ok: false as const, status: 401, user: null, ctx: null };

  const ctx = await getCompanyContext(supabase, user);
  /* Contas a pagar mostra o fornecedor de cada conta e precisa desta lista.
     Quem so pode ler fornecedor pelo Financeiro nao ganha a tela deles: ganha
     o nome para escrever a linha. */
  const liberado =
    requiredPermission === "suppliers.read"
      ? canReadSupplierLookup(ctx, user.id) || canCompany(ctx, user.id, "financial.payables.read")
      : canCompany(ctx, user.id, requiredPermission);
  if (!liberado) {
    return { ok: false as const, status: 403, user, ctx: null };
  }

  return { ok: true as const, status: 200, user, ctx };
}

async function resolveCategoryId(admin: ReturnType<typeof createAdminClient>, companyId: string, rawName: unknown) {
  const name = normalizeString(rawName);
  if (!name) return null;

  /* `%` e `_` sao curinga no ilike. Uma categoria chamada "Tinta 100%" virava
     um padrao de busca e podia casar com a categoria errada. */
  const semCuringa = name.replace(/[%_\\]/g, (caractere) => `\\${caractere}`);

  const { data: existing } = await admin
    .from("supplier_categories")
    .select("id")
    .eq("company_id", companyId)
    .ilike("name", semCuringa)
    .maybeSingle();

  if (existing?.id) return existing.id as string;

  const { data: created, error } = await admin
    .from("supplier_categories")
    .insert({
      company_id: companyId,
      name,
    })
    .select("id")
    .single();

  if (error || !created?.id) {
    throw new Error("Falha ao processar categoria do fornecedor.");
  }

  return created.id as string;
}

async function insertActivity(
  admin: ReturnType<typeof createAdminClient>,
  companyId: string,
  supplierId: string,
  actorUserId: string | null,
  entityType: "supplier" | "purchase_order" | "payable" | "import",
  action: string,
  description: string,
  payload: Record<string, unknown> = {}
) {
  await admin.from("supplier_activity_log").insert({
    company_id: companyId,
    supplier_id: supplierId,
    entity_type: entityType,
    action,
    description,
    payload,
    actor_user_id: actorUserId,
  });
}

async function upsertSupplier(
  admin: ReturnType<typeof createAdminClient>,
  companyId: string,
  actorUserId: string | null,
  raw: SupplierPayload,
  options?: { updateExistingByCnpj?: boolean }
) {
  const name = normalizeString(raw.name);
  const cnpj = documentoDoCorpo(raw).slice(0, 14);
  const email = normalizeString(raw.email);
  const phone = digits(raw.phone).slice(0, 11);
  const city = normalizeString(raw.city);

  /* A mesma checagem que a tela faz, para a resposta dizer exatamente o que
     falta em vez de "CNPJ inválido" para qualquer coisa. */
  const recusa = conferirFornecedor({ nome: name, documento: cnpj, email, telefone: phone });
  if (recusa) throw new Error(recusa.mensagem);

  const categoryId = await resolveCategoryId(admin, companyId, raw.category);

  const payload = {
    company_id: companyId,
    category_id: categoryId,
    name,
    legal_name: name,
    cnpj,
    document_type: tipoDoDocumento(cnpj),
    city,
    email,
    phone,
    payment_terms_primary: normalizeString(raw.paymentTermsPrimary) || "30 dias",
    payment_terms_secondary: normalizeString(raw.paymentTermsSecondary) || "Boleto",
    status: normalizeStatus(raw.status),
    notes: normalizeString(raw.notes) || null,
    created_by: actorUserId,
    deleted_at: null,
  };

  const { data: existing } = await admin
    .from("suppliers")
    .select("id,name")
    .eq("company_id", companyId)
    .eq("cnpj", cnpj)
    .is("deleted_at", null)
    .maybeSingle();

  if (existing?.id && options?.updateExistingByCnpj) {
    const { data: updated, error } = await admin
      .from("suppliers")
      .update(payload)
      .eq("id", existing.id)
      .eq("company_id", companyId)
      .select("id")
      .single();

    if (error || !updated?.id) throw new Error("Falha ao atualizar fornecedor na importação.");

    await insertActivity(
      admin,
      companyId,
      updated.id as string,
      actorUserId,
      "import",
      "import.updated",
      `Fornecedor ${name} atualizado via importação.`,
      { cnpj }
    );
    return { id: updated.id as string, mode: "updated" as const };
  }

  if (existing?.id) throw new Error("Já existe um fornecedor com este CNPJ.");

  const { data: created, error } = await admin
    .from("suppliers")
    .insert(payload)
    .select("id")
    .single();

  if (error || !created?.id) throw new Error("Falha ao criar fornecedor.");

  await insertActivity(
    admin,
    companyId,
    created.id as string,
    actorUserId,
    options?.updateExistingByCnpj ? "import" : "supplier",
    options?.updateExistingByCnpj ? "import.created" : "supplier.created",
    options?.updateExistingByCnpj ? `Fornecedor ${name} importado com sucesso.` : `Fornecedor ${name} cadastrado.`,
    { cnpj }
  );

  return { id: created.id as string, mode: "created" as const };
}

export async function GET() {
  const auth = await getAuth("suppliers.read");
  if (!auth.ok || !auth.ctx) {
    return NextResponse.json({ error: auth.status === 401 ? "Não autenticado." : "Sem permissão." }, { status: auth.status });
  }

  const admin = createAdminClient();
  const companyId = auth.ctx.companyId;

  const [
    { data: suppliers, error: suppliersError },
    { data: categories, error: categoriesError },
    { data: payables, error: payablesError },
    { data: profile, error: profileError },
    { data: ownerMembership, error: ownerMembershipError },
  ] =
    await Promise.all([
      admin
        .from("suppliers")
        .select("id,name,cnpj,document_type,city,email,phone,payment_terms_primary,payment_terms_secondary,status,notes,created_at,updated_at,category_id")
        .eq("company_id", companyId)
        .is("deleted_at", null)
        .order("created_at", { ascending: false }),
      admin.from("supplier_categories").select("id,name").eq("company_id", companyId).order("name", { ascending: true }),
      /* So o que esta em aberto: e o unico numero que a lista mostra, e
         puxar as pagas junto era trazer a tabela inteira do financeiro. */
      admin
        .from("accounts_payable")
        .select("supplier_id,amount,status")
        .eq("company_id", companyId)
        .not("supplier_id", "is", null)
        .in("status", ["PENDING", "OVERDUE"]),
      admin.from("profiles").select("address_city").eq("id", auth.user.id).maybeSingle(),
      admin.from("company_users").select("user_id,active").eq("company_id", companyId).eq("role", "owner").maybeSingle(),
    ]);

  if (suppliersError || categoriesError || payablesError || profileError) {
    return NextResponse.json({ error: "Falha ao carregar fornecedores." }, { status: 500 });
  }

  const ownerProfileId =
    auth.ctx.ownerId ||
    (!ownerMembershipError &&
    ownerMembership &&
    (ownerMembership as { active?: unknown; user_id?: unknown }).active !== false &&
    typeof (ownerMembership as { user_id?: unknown }).user_id === "string"
      ? String((ownerMembership as { user_id?: unknown }).user_id)
      : null);

  let ownerProfileCity: string | null = null;
  if (ownerProfileId && ownerProfileId !== auth.user.id) {
    const { data: ownerProfile } = await admin.from("profiles").select("address_city").eq("id", ownerProfileId).maybeSingle();
    if (ownerProfile && typeof ownerProfile.address_city === "string" && ownerProfile.address_city.trim()) {
      ownerProfileCity = ownerProfile.address_city;
    }
  }

  const categoryMap = new Map((categories ?? []).map((item) => [String(item.id), String(item.name || "")]));
  const payableSummary = new Map<string, { total: number; count: number }>();

  for (const row of payables ?? []) {
    const supplierId = typeof row.supplier_id === "string" ? row.supplier_id : "";
    if (!supplierId) continue;
    const status = String(row.status || "").toUpperCase();
    if (status !== "PENDING" && status !== "OVERDUE") continue;
    const current = payableSummary.get(supplierId) ?? { total: 0, count: 0 };
    current.total += Number(row.amount || 0);
    current.count += 1;
    payableSummary.set(supplierId, current);
  }

  const rows = (suppliers ?? []).map((row) => {
    const stats = payableSummary.get(String(row.id)) ?? { total: 0, count: 0 };
    return {
      id: String(row.id),
      name: String(row.name || ""),
      cnpj: String(row.cnpj || ""),
      documentType: row.document_type === "cpf" ? "cpf" : "cnpj",
      categoryId: row.category_id ? String(row.category_id) : null,
      category: row.category_id ? categoryMap.get(String(row.category_id)) || "Sem categoria" : "Sem categoria",
      city: String(row.city || ""),
      email: String(row.email || ""),
      phone: String(row.phone || ""),
      paymentTermsPrimary: String(row.payment_terms_primary || "30 dias"),
      paymentTermsSecondary: String(row.payment_terms_secondary || "Boleto"),
      openBillsTotal: Number(stats.total || 0),
      openBillsCount: Number(stats.count || 0),
      status: normalizeStatus(row.status),
      notes: typeof row.notes === "string" ? row.notes : null,
      createdAt: typeof row.created_at === "string" ? row.created_at : null,
      updatedAt: typeof row.updated_at === "string" ? row.updated_at : null,
    };
  });

  return NextResponse.json({
    ok: true,
    suppliers: rows,
    meta: {
      categories: (categories ?? []).map((item) => ({ id: String(item.id), name: String(item.name || "") })),
      companyCity:
        typeof profile?.address_city === "string" && profile.address_city.trim()
          ? profile.address_city
          : ownerProfileCity
            ? ownerProfileCity
            : null,
    },
  });
}

export async function POST(req: Request) {
  const auth = await getAuth("suppliers.list.write");
  if (!auth.ok || !auth.ctx) {
    return NextResponse.json({ error: auth.status === 401 ? "Não autenticado." : "Sem permissão." }, { status: auth.status });
  }

  const body = (await req.json().catch(() => null)) as
    | { items?: SupplierPayload[]; supplier?: SupplierPayload }
    | SupplierPayload
    | null;

  const admin = createAdminClient();
  const companyId = auth.ctx.companyId;

  const bodyItems =
    body && typeof body === "object" && "items" in body && Array.isArray(body.items) ? body.items : null;

  if (bodyItems && bodyItems.length > 0) {
    /* Antes o laco rodava fora de qualquer tratamento de erro: a linha 40 com
       problema derrubava a rota inteira depois de ter gravado as 39 primeiras,
       e o usuario recebia "Falha ao importar" sem saber onde parou. Agora cada
       linha responde por si e a resposta diz exatamente quais recusaram. */
    const criados: string[] = [];
    const atualizados: string[] = [];
    const recusados: Array<{ linha: number; nome: string; motivo: string }> = [];

    for (let indice = 0; indice < bodyItems.length; indice += 1) {
      const item = bodyItems[indice];
      try {
        const result = await upsertSupplier(admin, companyId, auth.user?.id ?? null, item, {
          updateExistingByCnpj: true,
        });
        if (result.mode === "updated") atualizados.push(result.id);
        else criados.push(result.id);
      } catch (erroDaLinha) {
        recusados.push({
          /* +1 porque a primeira linha do CSV e o cabecalho, entao o numero
             bate com o que a pessoa ve no editor. */
          linha: indice + 2,
          nome: normalizeString(item?.name) || "sem nome",
          motivo: erroDaLinha instanceof Error ? erroDaLinha.message : "Falha ao gravar.",
        });
      }
    }

    return NextResponse.json({
      ok: recusados.length === 0,
      imported: criados.length + atualizados.length,
      criados: criados.length,
      atualizados: atualizados.length,
      recusados,
    });
  }

  const payload = (body && "supplier" in body ? body.supplier : body) as SupplierPayload | null;
  if (!payload) return NextResponse.json({ error: "Payload inválido." }, { status: 400 });

  try {
    const created = await upsertSupplier(admin, companyId, auth.user?.id ?? null, payload);
    return NextResponse.json({ ok: true, id: created.id });
  } catch (error) {
    return NextResponse.json({ error: error instanceof Error ? error.message : "Falha ao criar fornecedor." }, { status: 400 });
  }
}

export async function PATCH(req: Request) {
  const auth = await getAuth("suppliers.list.write");
  if (!auth.ok || !auth.ctx) {
    return NextResponse.json({ error: auth.status === 401 ? "Não autenticado." : "Sem permissão." }, { status: auth.status });
  }

  const body = (await req.json().catch(() => null)) as
    | { id?: unknown; supplier?: SupplierPayload; ids?: unknown; status?: unknown }
    | null;

  const admin = createAdminClient();
  const companyId = auth.ctx.companyId;

  /* Ação em massa: a lista tinha caixinha de seleção em cada linha, caixinha
     mestre no topo e o contador "N selecionado(s)", e nenhuma ação consumia
     nada disso. Mudar o status de vários de uma vez é o que a seleção sempre
     prometeu. Excluir em massa fica de fora de propósito: apagar dezenas de
     fornecedores num clique é o tipo de coisa que ninguém desfaz. */
  if (Array.isArray(body?.ids)) {
    const ids = body.ids.map((valor) => normalizeString(valor)).filter(Boolean);
    if (ids.length === 0) return NextResponse.json({ error: "Selecione pelo menos um fornecedor." }, { status: 400 });
    if (typeof body?.status !== "string") {
      return NextResponse.json({ error: "Informe o novo status." }, { status: 400 });
    }

    const status = normalizeStatus(body.status);
    const { data: alterados, error: erroEmMassa } = await admin
      .from("suppliers")
      .update({ status })
      .eq("company_id", companyId)
      .in("id", ids)
      .is("deleted_at", null)
      .select("id,name");

    if (erroEmMassa) {
      return NextResponse.json({ error: "Falha ao mudar o status dos fornecedores." }, { status: 500 });
    }

    for (const linha of alterados ?? []) {
      await insertActivity(
        admin,
        companyId,
        String(linha.id),
        auth.user?.id ?? null,
        "supplier",
        "supplier.updated",
        `Fornecedor ${linha.name || "sem nome"} marcado como ${status}.`,
        { status, emMassa: true }
      );
    }

    return NextResponse.json({ ok: true, alterados: (alterados ?? []).length, status });
  }

  const id = normalizeString(body?.id);
  const supplier = body?.supplier;
  if (!id || !supplier) return NextResponse.json({ error: "Payload inválido." }, { status: 400 });

  const { data: existing } = await admin
    .from("suppliers")
    .select("id,name,cnpj")
    .eq("id", id)
    .eq("company_id", companyId)
    .is("deleted_at", null)
    .maybeSingle();

  if (!existing?.id) {
    return NextResponse.json({ error: "Fornecedor não encontrado." }, { status: 404 });
  }

  const name = normalizeString(supplier.name);
  const cnpj = documentoDoCorpo(supplier).slice(0, 14);
  const email = normalizeString(supplier.email);
  const phone = digits(supplier.phone).slice(0, 11);

  /* `documentoJaGravado` deixa quem quer so arrumar o telefone de um cadastro
     antigo salvar, mesmo que o documento gravado seja aquele com quatorze
     zeros. Trocar o documento continua exigindo um valido. */
  const recusa = conferirFornecedor(
    { nome: name, documento: cnpj, email, telefone: phone },
    { documentoJaGravado: String(existing.cnpj || "") }
  );
  if (recusa) return NextResponse.json({ error: recusa.mensagem }, { status: 400 });

  const { data: duplicate } = await admin
    .from("suppliers")
    .select("id")
    .eq("company_id", companyId)
    .eq("cnpj", cnpj)
    .is("deleted_at", null)
    .neq("id", id)
    .maybeSingle();

  if (duplicate?.id) {
    return NextResponse.json({ error: "Já existe outro fornecedor com este CNPJ." }, { status: 400 });
  }

  /* resolveCategoryId lanca quando o banco recusa a categoria. Sem isto, o
     erro subia como 500 sem mensagem em vez de dizer o que houve. */
  let categoryId: string | null = null;
  try {
    categoryId = await resolveCategoryId(admin, companyId, supplier.category);
  } catch (erroDaCategoria) {
    return NextResponse.json(
      { error: erroDaCategoria instanceof Error ? erroDaCategoria.message : "Falha ao processar categoria." },
      { status: 400 }
    );
  }

  const { error } = await admin
    .from("suppliers")
    .update({
      category_id: categoryId,
      name,
      legal_name: name,
      cnpj,
      document_type: tipoDoDocumento(cnpj),
      city: normalizeString(supplier.city),
      email,
      phone,
      payment_terms_primary: normalizeString(supplier.paymentTermsPrimary) || "30 dias",
      payment_terms_secondary: normalizeString(supplier.paymentTermsSecondary) || "Boleto",
      status: normalizeStatus(supplier.status),
      notes: normalizeString(supplier.notes) || null,
    })
    .eq("id", id)
    .eq("company_id", companyId);

  if (error) {
    return NextResponse.json({ error: "Falha ao atualizar fornecedor." }, { status: 500 });
  }

  await insertActivity(
    admin,
    companyId,
    id,
    auth.user?.id ?? null,
    "supplier",
    "supplier.updated",
    `Fornecedor ${name} atualizado.`,
    { cnpj }
  );

  return NextResponse.json({ ok: true });
}

export async function DELETE(req: Request) {
  const auth = await getAuth("suppliers.list.write");
  if (!auth.ok || !auth.ctx) {
    return NextResponse.json({ error: auth.status === 401 ? "Não autenticado." : "Sem permissão." }, { status: auth.status });
  }

  const body = (await req.json().catch(() => null)) as { id?: unknown } | null;
  const id = normalizeString(body?.id);
  if (!id) return NextResponse.json({ error: "ID inválido." }, { status: 400 });

  const admin = createAdminClient();
  const companyId = auth.ctx.companyId;

  const { data: existing } = await admin
    .from("suppliers")
    .select("id,name")
    .eq("id", id)
    .eq("company_id", companyId)
    .is("deleted_at", null)
    .maybeSingle();

  if (!existing?.id) return NextResponse.json({ error: "Fornecedor não encontrado." }, { status: 404 });

  const { error } = await admin
    .from("suppliers")
    .update({
      deleted_at: new Date().toISOString(),
      status: "inativo",
    })
    .eq("id", id)
    .eq("company_id", companyId);

  if (error) return NextResponse.json({ error: "Falha ao remover fornecedor." }, { status: 500 });

  await insertActivity(
    admin,
    companyId,
    id,
    auth.user?.id ?? null,
    "supplier",
    "supplier.deleted",
    `Fornecedor ${existing.name || "sem nome"} removido da listagem.`,
    {}
  );

  return NextResponse.json({ ok: true });
}
