import type { FastifyInstance } from "fastify";
import { z } from "zod";

import { AppError } from "../lib/errors.js";
import { supabaseAdmin } from "../lib/supabase.js";
import { isoNow } from "../lib/utils.js";
import { requireOrganizationMembership, requireUser } from "../services/auth-service.js";
import { logContactActivities, logContactActivity } from "../services/activity-log-service.js";
import { dedupeContacts, parseCsvContacts, parseTxtContacts } from "../services/contact-import.js";
import { enforceContactQuota, syncContactUsage } from "../services/quota-service.js";

const contactStatusSchema = z.enum(["subscribed", "unsubscribed", "bounced", "complained"]);
const contactBodySchema = z.object({
  organizationId: z.string().uuid(),
  email: z.string().email(),
  name: z.string().trim().max(160).optional().nullable(),
  firstName: z.string().trim().max(80).optional().nullable(),
  lastName: z.string().trim().max(80).optional().nullable(),
  alternateEmail: z.string().trim().email().optional().nullable().or(z.literal("")),
  phone: z.string().trim().max(60).optional().nullable(),
  source: z.string().trim().max(120).optional().nullable(),
  company: z.string().trim().max(160).optional().nullable(),
  jobTitle: z.string().trim().max(120).optional().nullable(),
  country: z.string().trim().max(120).optional().nullable(),
  city: z.string().trim().max(120).optional().nullable(),
  lifecycleStage: z.string().trim().max(80).optional().nullable(),
  notes: z.string().trim().max(2000).optional().nullable(),
  status: contactStatusSchema,
  tags: z.array(z.string().trim().min(1).max(60)).optional()
});

function normalizeContactPayload(body: z.infer<typeof contactBodySchema>) {
  const firstName = body.firstName?.trim() || null;
  const lastName = body.lastName?.trim() || null;
  const fullName = body.name?.trim() || [firstName, lastName].filter(Boolean).join(" ").trim() || null;

  return {
    email: body.email.trim().toLowerCase(),
    full_name: fullName,
    first_name: firstName,
    last_name: lastName,
    alternate_email: body.alternateEmail?.trim() ? body.alternateEmail.trim().toLowerCase() : null,
    phone: body.phone?.trim() || null,
    source: body.source?.trim() || null,
    company: body.company?.trim() || null,
    job_title: body.jobTitle?.trim() || null,
    country: body.country?.trim() || null,
    city: body.city?.trim() || null,
    lifecycle_stage: body.lifecycleStage?.trim() || null,
    notes: body.notes?.trim() || null,
    status: body.status,
    tags: body.tags ?? [],
    last_activity_at: isoNow(),
    updated_at: isoNow()
  };
}

function buildManualStatusTracking(nextStatus: z.infer<typeof contactStatusSchema>, previousStatus?: string | null) {
  const now = isoNow();
  const changed = !previousStatus || previousStatus !== nextStatus;
  const patch: Record<string, unknown> = {};

  if (changed) {
    patch.last_status_changed_at = now;
    patch.status_source = "manual";
    patch.status_reason = "updated_from_contacts_workspace";
  }

  if (nextStatus === "unsubscribed" && changed) {
    patch.unsubscribed_at = now;
    patch.unsubscribed_via = "manual";
  }

  if (nextStatus === "bounced" && changed) {
    patch.bounced_at = now;
  }

  if (nextStatus === "complained" && changed) {
    patch.complained_at = now;
  }

  return patch;
}

function summarizeContactForLog(contact: Record<string, any>) {
  return {
    email: String(contact.email ?? ""),
    fullName: String(contact.full_name ?? ""),
    status: String(contact.status ?? ""),
    source: String(contact.source ?? ""),
    company: String(contact.company ?? ""),
    lifecycleStage: String(contact.lifecycle_stage ?? ""),
    tagCount: Array.isArray(contact.tags) ? contact.tags.length : 0,
  };
}

function changedContactFields(before: Record<string, any>, after: Record<string, any>) {
  const keys = [
    "email",
    "full_name",
    "first_name",
    "last_name",
    "alternate_email",
    "phone",
    "source",
    "company",
    "job_title",
    "country",
    "city",
    "lifecycle_stage",
    "notes",
    "status",
    "tags",
  ];

  return keys.filter((key) => JSON.stringify(before[key] ?? null) !== JSON.stringify(after[key] ?? null));
}

export function registerContactRoutes(app: FastifyInstance) {
  app.get("/api/contacts", async (request) => {
    const query = z.object({ organizationId: z.string().uuid() }).parse(request.query);
    await requireOrganizationMembership(request, query.organizationId);

    const pageSize = 1000;
    const allContacts: Record<string, unknown>[] = [];
    let from = 0;

    while (true) {
      const { data, error } = await supabaseAdmin
        .from("contacts")
        .select("*")
        .eq("organization_id", query.organizationId)
        .order("created_at", { ascending: false })
        .range(from, from + pageSize - 1);

      if (error) {
        throw new AppError(400, "CONTACT_LIST_FAILED", error.message);
      }

      const batch = (data ?? []) as Record<string, unknown>[];
      allContacts.push(...batch);

      if (batch.length < pageSize) {
        break;
      }

      from += pageSize;
    }

    return {
      items: allContacts
    };
  });

  app.post("/api/contacts", async (request) => {
    const body = contactBodySchema.parse(request.body);
    await requireOrganizationMembership(request, body.organizationId);
    const user = await requireUser(request);
    await enforceContactQuota(body.organizationId, 1);

    const payload = {
      organization_id: body.organizationId,
      created_by_auth_user_id: user.id,
      updated_by_auth_user_id: user.id,
      created_at: isoNow(),
      ...normalizeContactPayload(body),
      ...buildManualStatusTracking(body.status)
    };

    const { data, error } = await supabaseAdmin
      .from("contacts")
      .insert(payload)
      .select("*")
      .single();

    if (error || !data) {
      throw new AppError(400, "CONTACT_CREATE_FAILED", error?.message ?? "Unable to create contact.");
    }

    await syncContactUsage(body.organizationId);
    await logContactActivity({
      organizationId: body.organizationId,
      contactId: String(data.id),
      actorAuthUserId: user.id,
      action: "contact.created",
      metadata: summarizeContactForLog(data as Record<string, any>)
    });

    return { contact: data };
  });

  app.put("/api/contacts/:contactId", async (request) => {
    const params = z.object({ contactId: z.string().uuid() }).parse(request.params);
    const body = contactBodySchema.parse(request.body);

    await requireOrganizationMembership(request, body.organizationId);
    const user = await requireUser(request);
    const { data: existingContact, error: existingContactError } = await supabaseAdmin
      .from("contacts")
      .select("*")
      .eq("id", params.contactId)
      .eq("organization_id", body.organizationId)
      .single();

    if (existingContactError || !existingContact) {
      throw new AppError(404, "CONTACT_NOT_FOUND", existingContactError?.message ?? "Contact not found.");
    }

    const payload = {
      ...normalizeContactPayload(body),
      updated_by_auth_user_id: user.id,
      last_activity_at: isoNow(),
      ...buildManualStatusTracking(body.status, String(existingContact.status ?? "subscribed"))
    };

    const { data, error } = await supabaseAdmin
      .from("contacts")
      .update(payload)
      .eq("id", params.contactId)
      .eq("organization_id", body.organizationId)
      .select("*")
      .single();

    if (error || !data) {
      throw new AppError(400, "CONTACT_UPDATE_FAILED", error?.message ?? "Unable to update contact.");
    }

    await logContactActivity({
      organizationId: body.organizationId,
      contactId: params.contactId,
      actorAuthUserId: user.id,
      action: "contact.updated",
      metadata: {
        changedFields: changedContactFields(existingContact as Record<string, any>, data as Record<string, any>),
        before: summarizeContactForLog(existingContact as Record<string, any>),
        after: summarizeContactForLog(data as Record<string, any>),
      }
    });

    return { contact: data };
  });

  app.delete("/api/contacts/:contactId", async (request) => {
    const params = z.object({ contactId: z.string().uuid() }).parse(request.params);
    const query = z.object({ organizationId: z.string().uuid() }).parse(request.query);

    await requireOrganizationMembership(request, query.organizationId);
    const user = await requireUser(request);
    const { data: existingContact, error: existingContactError } = await supabaseAdmin
      .from("contacts")
      .select("*")
      .eq("id", params.contactId)
      .eq("organization_id", query.organizationId)
      .single();

    if (existingContactError || !existingContact) {
      throw new AppError(404, "CONTACT_NOT_FOUND", existingContactError?.message ?? "Contact not found.");
    }

    const { error } = await supabaseAdmin
      .from("contacts")
      .delete()
      .eq("id", params.contactId)
      .eq("organization_id", query.organizationId);

    if (error) {
      throw new AppError(400, "CONTACT_DELETE_FAILED", error.message);
    }

    await syncContactUsage(query.organizationId);
    await logContactActivity({
      organizationId: query.organizationId,
      contactId: params.contactId,
      actorAuthUserId: user.id,
      action: "contact.deleted",
      metadata: summarizeContactForLog(existingContact as Record<string, any>)
    });

    return { success: true };
  });

  app.post("/api/contacts/import", async (request) => {
    const jsonBody = request.body as
      | {
          organizationId?: string;
          format?: "csv" | "txt";
          content?: string;
          filename?: string;
        }
      | undefined;
    const multipartBody = request.body as
      | {
          organizationId?: string | { value?: string };
          file?: {
            filename?: string;
            value?: Buffer | string;
            _buf?: Buffer;
          };
        }
      | undefined;
    let organizationId = jsonBody?.organizationId ?? "";
    let fileName = jsonBody?.filename ?? `contacts.${jsonBody?.format ?? "txt"}`;
    let rawContent = jsonBody?.content ?? "";

    if (!rawContent) {
      const parts = request.parts();
      organizationId =
        typeof multipartBody?.organizationId === "string"
          ? multipartBody.organizationId
          : multipartBody?.organizationId?.value ?? organizationId;

      for await (const part of parts) {
        if (part.type === "file") {
          fileName = part.filename;
          const chunks: Buffer[] = [];
          for await (const chunk of part.file) {
            chunks.push(Buffer.from(chunk));
          }
          rawContent = Buffer.concat(chunks).toString("utf8");
        } else if (part.fieldname === "organizationId") {
          organizationId = String(part.value ?? "");
        }
      }

      if (!rawContent && multipartBody?.file) {
        fileName = multipartBody.file.filename ?? fileName;
        rawContent = Buffer.isBuffer(multipartBody.file.value)
          ? multipartBody.file.value.toString("utf8")
          : typeof multipartBody.file.value === "string"
            ? multipartBody.file.value
            : Buffer.isBuffer(multipartBody.file._buf)
              ? multipartBody.file._buf.toString("utf8")
              : rawContent;
      }
    }

    if (!organizationId) {
      throw new AppError(400, "ORGANIZATION_REQUIRED", "organizationId is required.");
    }

    if (!rawContent.trim()) {
      throw new AppError(400, "CONTACT_IMPORT_EMPTY", "Add at least one contact before importing.");
    }

    await requireOrganizationMembership(request, organizationId);
    const user = await requireUser(request);

    const imported = fileName.toLowerCase().endsWith(".csv") ? parseCsvContacts(rawContent) : parseTxtContacts(rawContent);
    const contacts = dedupeContacts(imported);

    if (!contacts.length) {
      throw new AppError(400, "CONTACT_IMPORT_EMPTY", "No valid contacts were found in the import.");
    }

    const contactEmails = contacts.map((contact) => contact.email);
    const { data: existingContacts, error: existingContactsError } = await supabaseAdmin
      .from("contacts")
      .select("id, email, full_name, status, source, company, lifecycle_stage, tags")
      .eq("organization_id", organizationId)
      .in("email", contactEmails);

    if (existingContactsError) {
      throw new AppError(400, "CONTACT_IMPORT_LOOKUP_FAILED", existingContactsError.message);
    }

    const existingByEmail = new Map(
      ((existingContacts ?? []) as Array<Record<string, any>>).map((contact) => [String(contact.email ?? "").toLowerCase(), contact])
    );

    const newContacts = contacts.filter((contact) => !existingByEmail.has(contact.email));
    await enforceContactQuota(organizationId, newContacts.length);

    const { data: job, error: jobError } = await supabaseAdmin
      .from("contact_import_jobs")
      .insert({
        organization_id: organizationId,
        file_name: fileName,
        total_rows: contacts.length,
        created_by_auth_user_id: user.id,
        status: "processing",
        imported_count: 0,
        created_at: isoNow(),
        updated_at: isoNow()
      })
      .select("id")
      .single();

    if (jobError || !job) {
      throw new AppError(400, "IMPORT_JOB_FAILED", jobError?.message ?? "Unable to create import job.");
    }

    const insertedAt = isoNow();
    const newRows = newContacts.map((contact) => ({
      organization_id: organizationId,
      email: contact.email,
      full_name: contact.full_name,
      first_name: contact.first_name,
      last_name: contact.last_name,
      alternate_email: contact.alternate_email,
      phone: contact.phone,
      source: contact.source,
      company: contact.company,
      job_title: contact.job_title,
      tags: contact.tags,
      country: contact.country,
      city: contact.city,
      lifecycle_stage: contact.lifecycle_stage,
      notes: contact.notes,
      status: "subscribed",
      created_by_auth_user_id: user.id,
      updated_by_auth_user_id: user.id,
      last_import_job_id: job.id,
      last_imported_at: insertedAt,
      last_imported_by_auth_user_id: user.id,
      last_activity_at: insertedAt,
      created_at: insertedAt,
      updated_at: insertedAt
    }));

    let insertedContacts: Array<Record<string, any>> = [];

    if (newRows.length) {
      const { data: inserted, error: insertError } = await supabaseAdmin
        .from("contacts")
        .insert(newRows)
        .select("*");

      if (insertError) {
        throw new AppError(400, "CONTACT_IMPORT_FAILED", insertError.message);
      }

      insertedContacts = (inserted ?? []) as Array<Record<string, any>>;
    }

    const updatedContacts: Array<Record<string, any>> = [];
    for (const importedContact of contacts.filter((contact) => existingByEmail.has(contact.email))) {
      const existingContact = existingByEmail.get(importedContact.email) as Record<string, any>;
      const updatePayload = {
        full_name: importedContact.full_name,
        first_name: importedContact.first_name,
        last_name: importedContact.last_name,
        alternate_email: importedContact.alternate_email,
        phone: importedContact.phone,
        source: importedContact.source,
        company: importedContact.company,
        job_title: importedContact.job_title,
        tags: importedContact.tags,
        country: importedContact.country,
        city: importedContact.city,
        lifecycle_stage: importedContact.lifecycle_stage,
        notes: importedContact.notes,
        status: "subscribed",
        updated_by_auth_user_id: user.id,
        last_import_job_id: job.id,
        last_imported_at: insertedAt,
        last_imported_by_auth_user_id: user.id,
        last_activity_at: insertedAt,
        updated_at: insertedAt
      };

      const { data: updatedContact, error: updateError } = await supabaseAdmin
        .from("contacts")
        .update(updatePayload)
        .eq("id", existingContact.id)
        .eq("organization_id", organizationId)
        .select("*")
        .single();

      if (updateError || !updatedContact) {
        throw new AppError(400, "CONTACT_IMPORT_FAILED", updateError?.message ?? "Unable to update imported contact.");
      }

      updatedContacts.push(updatedContact as Record<string, any>);
    }

    const totalAffectedRows = insertedContacts.length + updatedContacts.length;

    await supabaseAdmin
      .from("contact_import_jobs")
      .update({
        status: "completed",
        imported_count: totalAffectedRows,
        completed_by_auth_user_id: user.id,
        updated_at: isoNow()
      })
      .eq("id", job.id);

    await syncContactUsage(organizationId);
    await logContactActivities([
      ...insertedContacts.map((contact) => ({
        organizationId,
        contactId: String(contact.id),
        importJobId: String(job.id),
        actorAuthUserId: user.id,
        action: "contact.import_created",
        metadata: {
          fileName,
          ...summarizeContactForLog(contact),
        }
      })),
      ...updatedContacts.map((contact) => ({
        organizationId,
        contactId: String(contact.id),
        importJobId: String(job.id),
        actorAuthUserId: user.id,
        action: "contact.import_updated",
        metadata: {
          fileName,
          ...summarizeContactForLog(contact),
        }
      }))
    ]);
    await logContactActivity({
      organizationId,
      importJobId: String(job.id),
      actorAuthUserId: user.id,
      action: "contact.import_completed",
      metadata: {
        fileName,
        totalRows: contacts.length,
        createdCount: insertedContacts.length,
        updatedCount: updatedContacts.length,
      }
    });

    return {
      success: true,
      importedCount: totalAffectedRows
    };
  });
}
