import { randomBytes } from "@noble/hashes/utils"
import { ConvexError, v } from "convex/values"

import { mutation, query } from "./_generated/server"
import type { Id } from "./_generated/dataModel"
import type { MutationCtx, QueryCtx } from "./_generated/server"

const SECRET = process.env.CONVEX_SYNC_SECRET ?? "dev-sync-secret"

const VALID_ROLES = new Set(["ADMIN", "MANAGER", "AGENT", "COLLABORATOR"])
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"])

function normalizeEmail(value: string) {
  return value.trim().toLowerCase()
}

function generateProvisioningCode() {
  return Array.from(randomBytes(32), (b) => b.toString(16).padStart(2, "0")).join("")
}

type ImportedUser = {
  email: string
  name: string
  role?: string | null
  avatarUrl?: string | null
  teams?: string[] | null
  companySlug?: string | null
}

type ImportedQueue = {
  slug?: string | null
  name: string
}

type ImportedCompany = {
  slug: string
  name: string
  provisioningCode?: string | null
  isAvulso?: boolean | null
  cnpj?: string | null
  domain?: string | null
  phone?: string | null
  description?: string | null
  address?: string | null
  createdAt?: number | null
  updatedAt?: number | null
}

function normalizeRole(role: string | null | undefined) {
  if (!role) return "AGENT"
  const normalized = role.toUpperCase()
  if (VALID_ROLES.has(normalized)) return normalized
  // map legacy CUSTOMER to MANAGER
  if (normalized === "CUSTOMER") return "MANAGER"
  return "AGENT"
}

function slugify(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^\w\s-]/g, "")
    .trim()
    .replace(/\s+/g, "-")
    .replace(/-+/g, "-")
    .toLowerCase()
}

function pruneUndefined<T extends Record<string, unknown>>(input: T): T {
  for (const key of Object.keys(input)) {
    if (input[key] === undefined) {
      delete input[key]
    }
  }
  return input
}

async function ensureUser(
  ctx: MutationCtx,
  tenantId: string,
  data: ImportedUser,
  cache: Map<string, Id<"users">>,
  companyCache: Map<string, Id<"companies">>
) {
  if (cache.has(data.email)) {
    return cache.get(data.email)!
  }
  const existing = await ctx.db
    .query("users")
    .withIndex("by_tenant_email", (q) => q.eq("tenantId", tenantId).eq("email", data.email))
    .first()

  const role = normalizeRole(data.role)
  const companyId = data.companySlug ? companyCache.get(data.companySlug) : undefined
  const record = existing
    ? (() => {
        const needsPatch =
          existing.name !== data.name ||
          existing.role !== role ||
          existing.avatarUrl !== (data.avatarUrl ?? undefined) ||
          JSON.stringify(existing.teams ?? []) !== JSON.stringify(data.teams ?? []) ||
          (existing.companyId ?? undefined) !== companyId
        if (needsPatch) {
          return ctx.db.patch(existing._id, {
            name: data.name,
            role,
            avatarUrl: data.avatarUrl ?? undefined,
            teams: data.teams ?? undefined,
            tenantId,
            companyId,
          })
        }
        return Promise.resolve()
      })()
    : ctx.db.insert("users", {
        tenantId,
        email: data.email,
        name: data.name,
        role,
        avatarUrl: data.avatarUrl ?? undefined,
        teams: data.teams ?? undefined,
        companyId,
      })

  const id = existing ? existing._id : ((await record) as Id<"users">)
  cache.set(data.email, id)
  return id
}

async function ensureQueue(
  ctx: MutationCtx,
  tenantId: string,
  data: ImportedQueue,
  cache: Map<string, Id<"queues">>
) {
  const slug = data.slug && data.slug.trim().length > 0 ? data.slug : slugify(data.name)
  if (cache.has(slug)) return cache.get(slug)!

  const bySlug = await ctx.db
    .query("queues")
    .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug))
    .first()
  if (bySlug) {
    if (bySlug.name !== data.name) {
      await ctx.db.patch(bySlug._id, { name: data.name })
    }
    cache.set(slug, bySlug._id)
    return bySlug._id
  }

  const byName = await ctx.db
    .query("queues")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .filter((q) => q.eq(q.field("name"), data.name))
    .first()
  if (byName) {
    if (byName.slug !== slug) {
      await ctx.db.patch(byName._id, { slug })
    }
    cache.set(slug, byName._id)
    return byName._id
  }

  const id = await ctx.db.insert("queues", {
    tenantId,
    name: data.name,
    slug,
    teamId: undefined,
  })
  cache.set(slug, id)
  return id
}

async function ensureCompany(
  ctx: MutationCtx,
  tenantId: string,
  data: ImportedCompany,
  cache: Map<string, Id<"companies">>
) {
  const slug = data.slug || slugify(data.name)
  if (cache.has(slug)) {
    return cache.get(slug)!
  }

  const existing = await ctx.db
    .query("companies")
    .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug))
    .first()

  const payload = pruneUndefined({
    tenantId,
    name: data.name,
    slug,
    provisioningCode: data.provisioningCode ?? existing?.provisioningCode ?? generateProvisioningCode(),
    isAvulso: data.isAvulso ?? undefined,
    cnpj: data.cnpj ?? undefined,
    domain: data.domain ?? undefined,
    phone: data.phone ?? undefined,
    description: data.description ?? undefined,
    address: data.address ?? undefined,
    createdAt: data.createdAt ?? Date.now(),
    updatedAt: data.updatedAt ?? Date.now(),
  })

  let id: Id<"companies">
  if (existing) {
    const existingIsAvulso = existing.isAvulso ?? undefined
    const targetIsAvulso = payload.isAvulso ?? existingIsAvulso
    const targetCnpj = payload.cnpj ?? undefined
    const targetDomain = payload.domain ?? undefined
    const targetPhone = payload.phone ?? undefined
    const targetDescription = payload.description ?? undefined
    const targetAddress = payload.address ?? undefined

    const needsPatch =
      existing.name !== payload.name ||
      existingIsAvulso !== targetIsAvulso ||
      (existing.cnpj ?? undefined) !== targetCnpj ||
      (existing.domain ?? undefined) !== targetDomain ||
      (existing.phone ?? undefined) !== targetPhone ||
      (existing.description ?? undefined) !== targetDescription ||
      (existing.address ?? undefined) !== targetAddress ||
      existing.provisioningCode !== payload.provisioningCode
    if (needsPatch) {
      await ctx.db.patch(existing._id, {
        name: payload.name,
        isAvulso: targetIsAvulso,
        cnpj: targetCnpj,
        domain: targetDomain,
        phone: targetPhone,
        description: targetDescription,
        address: targetAddress,
        provisioningCode: payload.provisioningCode,
        updatedAt: Date.now(),
      })
    }
    id = existing._id
  } else {
    id = await ctx.db.insert("companies", payload)
  }

  cache.set(slug, id)
  return id
}

async function getTenantUsers(ctx: QueryCtx, tenantId: string) {
  return ctx.db
    .query("users")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .collect()
}

async function getTenantQueues(ctx: QueryCtx, tenantId: string) {
  return ctx.db
    .query("queues")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .collect()
}

async function getTenantCompanies(ctx: QueryCtx, tenantId: string) {
  return ctx.db
    .query("companies")
    .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
    .collect()
}

export const exportTenantSnapshot = query({
  args: {
    secret: v.string(),
    tenantId: v.string(),
  },
  handler: async (ctx, { secret, tenantId }) => {
    if (secret !== SECRET) {
      throw new ConvexError("CONVEX_SYNC_SECRET não configurada no backend")
    }

    const [users, queues, companies] = await Promise.all([
      getTenantUsers(ctx, tenantId),
      getTenantQueues(ctx, tenantId),
      getTenantCompanies(ctx, tenantId),
    ])

    const userMap = new Map(users.map((user) => [user._id, user]))
    const queueMap = new Map(queues.map((queue) => [queue._id, queue]))
    const companyMap = new Map(companies.map((company) => [company._id, company]))

    const tickets = await ctx.db
      .query("tickets")
      .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
      .collect()

    const ticketsWithRelations = []

    for (const ticket of tickets) {
      const comments = await ctx.db
        .query("ticketComments")
        .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
        .collect()

      const events = await ctx.db
        .query("ticketEvents")
        .withIndex("by_ticket", (q) => q.eq("ticketId", ticket._id))
        .collect()

      const requester = userMap.get(ticket.requesterId)
      const assignee = ticket.assigneeId ? userMap.get(ticket.assigneeId) : undefined
      const queue = ticket.queueId ? queueMap.get(ticket.queueId) : undefined
      const company = ticket.companyId
        ? companyMap.get(ticket.companyId)
        : requester?.companyId
        ? companyMap.get(requester.companyId)
        : undefined

      if (!requester) {
        continue
      }

      ticketsWithRelations.push({
        reference: ticket.reference,
        subject: ticket.subject,
        summary: ticket.summary ?? null,
        status: ticket.status,
        priority: ticket.priority,
        channel: ticket.channel,
        queueSlug: queue?.slug ?? undefined,
        requesterEmail: requester.email,
        assigneeEmail: assignee?.email ?? undefined,
        companySlug: company?.slug ?? undefined,
        dueAt: ticket.dueAt ?? undefined,
        firstResponseAt: ticket.firstResponseAt ?? undefined,
        resolvedAt: ticket.resolvedAt ?? undefined,
        closedAt: ticket.closedAt ?? undefined,
        createdAt: ticket.createdAt,
        updatedAt: ticket.updatedAt,
        tags: ticket.tags ?? [],
        comments: comments
          .map((comment) => {
            const author = userMap.get(comment.authorId)
            if (!author) {
              return null
            }
            return {
              authorEmail: author.email,
              visibility: comment.visibility,
              body: comment.body,
              createdAt: comment.createdAt,
              updatedAt: comment.updatedAt,
            }
          })
          .filter((value): value is {
            authorEmail: string
            visibility: string
            body: string
            createdAt: number
            updatedAt: number
          } => value !== null),
        events: events.map((event) => ({
          type: event.type,
          payload: event.payload ?? {},
          createdAt: event.createdAt,
        })),
      })
    }

    return {
      tenantId,
      companies: companies.map((company) => ({
        slug: company.slug,
        name: company.name,
        isAvulso: company.isAvulso ?? false,
        cnpj: company.cnpj ?? null,
        domain: company.domain ?? null,
        phone: company.phone ?? null,
        description: company.description ?? null,
        address: company.address ?? null,
        createdAt: company.createdAt,
        updatedAt: company.updatedAt,
      })),
      users: users.map((user) => ({
        email: user.email,
        name: user.name,
        role: user.role ?? null,
        avatarUrl: user.avatarUrl ?? null,
        teams: user.teams ?? [],
        companySlug: user.companyId ? companyMap.get(user.companyId)?.slug ?? null : null,
      })),
      queues: queues.map((queue) => ({
        name: queue.name,
        slug: queue.slug,
      })),
      tickets: ticketsWithRelations,
    }
  },
})

export const importPrismaSnapshot = mutation({
  args: {
    secret: v.string(),
    snapshot: v.object({
      tenantId: v.string(),
      companies: v.array(
        v.object({
          slug: v.string(),
          name: v.string(),
          cnpj: v.optional(v.string()),
          domain: v.optional(v.string()),
          phone: v.optional(v.string()),
          description: v.optional(v.string()),
          address: v.optional(v.string()),
          createdAt: v.optional(v.number()),
          updatedAt: v.optional(v.number()),
        })
      ),
      users: v.array(
        v.object({
          email: v.string(),
          name: v.string(),
          role: v.optional(v.string()),
          avatarUrl: v.optional(v.string()),
          teams: v.optional(v.array(v.string())),
          companySlug: v.optional(v.string()),
        })
      ),
      queues: v.array(
        v.object({
          name: v.string(),
          slug: v.optional(v.string()),
        })
      ),
      tickets: v.array(
        v.object({
          reference: v.number(),
          subject: v.string(),
          summary: v.optional(v.string()),
          status: v.string(),
          priority: v.string(),
          channel: v.string(),
          queueSlug: v.optional(v.string()),
          requesterEmail: v.string(),
          assigneeEmail: v.optional(v.string()),
          companySlug: v.optional(v.string()),
          dueAt: v.optional(v.number()),
          firstResponseAt: v.optional(v.number()),
          resolvedAt: v.optional(v.number()),
          closedAt: v.optional(v.number()),
          createdAt: v.number(),
          updatedAt: v.number(),
          tags: v.optional(v.array(v.string())),
          comments: v.array(
            v.object({
              authorEmail: v.string(),
              visibility: v.string(),
              body: v.string(),
              createdAt: v.number(),
              updatedAt: v.number(),
            })
          ),
          events: v.array(
            v.object({
              type: v.string(),
              payload: v.optional(v.any()),
              createdAt: v.number(),
            })
          ),
        })
      ),
    }),
  },
  handler: async (ctx, { secret, snapshot }) => {
    if (!SECRET) {
      throw new ConvexError("CONVEX_SYNC_SECRET não configurada no backend")
    }
    if (secret !== SECRET) {
      throw new ConvexError("Segredo inválido para sincronização")
    }

    const companyCache = new Map<string, Id<"companies">>()
    const userCache = new Map<string, Id<"users">>()
    const queueCache = new Map<string, Id<"queues">>()

    for (const company of snapshot.companies) {
      await ensureCompany(ctx, snapshot.tenantId, company, companyCache)
    }

    for (const user of snapshot.users) {
      await ensureUser(ctx, snapshot.tenantId, user, userCache, companyCache)
    }

    for (const queue of snapshot.queues) {
      await ensureQueue(ctx, snapshot.tenantId, queue, queueCache)
    }

    const snapshotStaffEmails = new Set(
      snapshot.users
        .filter((user) => INTERNAL_STAFF_ROLES.has(normalizeRole(user.role ?? null)))
        .map((user) => normalizeEmail(user.email))
    )

    const existingTenantUsers = await ctx.db
      .query("users")
      .withIndex("by_tenant", (q) => q.eq("tenantId", snapshot.tenantId))
      .collect()

    for (const user of existingTenantUsers) {
      const role = normalizeRole(user.role ?? null)
      if (INTERNAL_STAFF_ROLES.has(role) && !snapshotStaffEmails.has(normalizeEmail(user.email))) {
        await ctx.db.delete(user._id)
      }
    }

    let ticketsUpserted = 0
    let commentsInserted = 0
    let eventsInserted = 0

    for (const ticket of snapshot.tickets) {
      const requesterId = await ensureUser(
        ctx,
        snapshot.tenantId,
        {
          email: ticket.requesterEmail,
          name: ticket.requesterEmail,
        },
        userCache,
        companyCache
      )
      const assigneeId = ticket.assigneeEmail
        ? await ensureUser(
            ctx,
            snapshot.tenantId,
            {
              email: ticket.assigneeEmail,
              name: ticket.assigneeEmail,
            },
            userCache,
            companyCache
          )
        : undefined

      const queueId = ticket.queueSlug ? queueCache.get(ticket.queueSlug) ?? (await ensureQueue(ctx, snapshot.tenantId, { name: ticket.queueSlug, slug: ticket.queueSlug }, queueCache)) : undefined
      const companyId = ticket.companySlug ? companyCache.get(ticket.companySlug) ?? (await ensureCompany(ctx, snapshot.tenantId, { slug: ticket.companySlug, name: ticket.companySlug }, companyCache)) : undefined

      const existing = await ctx.db
        .query("tickets")
        .withIndex("by_tenant_reference", (q) => q.eq("tenantId", snapshot.tenantId).eq("reference", ticket.reference))
        .first()

      const payload = pruneUndefined({
        tenantId: snapshot.tenantId,
        reference: ticket.reference,
        subject: ticket.subject,
        summary: ticket.summary ?? undefined,
        status: ticket.status,
        priority: ticket.priority,
        channel: ticket.channel,
        queueId: queueId as Id<"queues"> | undefined,
        categoryId: undefined,
        subcategoryId: undefined,
        requesterId,
        assigneeId: assigneeId as Id<"users"> | undefined,
        working: false,
        slaPolicyId: undefined,
        companyId: companyId as Id<"companies"> | undefined,
        dueAt: ticket.dueAt ?? undefined,
        firstResponseAt: ticket.firstResponseAt ?? undefined,
        resolvedAt: ticket.resolvedAt ?? undefined,
        closedAt: ticket.closedAt ?? undefined,
        updatedAt: ticket.updatedAt,
        createdAt: ticket.createdAt,
        tags: ticket.tags && ticket.tags.length > 0 ? ticket.tags : undefined,
        customFields: undefined,
        totalWorkedMs: undefined,
        activeSessionId: undefined,
      })

      let ticketId: Id<"tickets">
      if (existing) {
        await ctx.db.patch(existing._id, payload)
        ticketId = existing._id
      } else {
        ticketId = await ctx.db.insert("tickets", payload)
      }

      ticketsUpserted += 1

      const existingComments = await ctx.db
        .query("ticketComments")
        .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
        .collect()
      for (const comment of existingComments) {
        await ctx.db.delete(comment._id)
      }

      const existingEvents = await ctx.db
        .query("ticketEvents")
        .withIndex("by_ticket", (q) => q.eq("ticketId", ticketId))
        .collect()
      for (const event of existingEvents) {
        await ctx.db.delete(event._id)
      }

      for (const comment of ticket.comments) {
        const authorId = await ensureUser(
          ctx,
          snapshot.tenantId,
          {
            email: comment.authorEmail,
            name: comment.authorEmail,
          },
          userCache,
          companyCache
        )
        await ctx.db.insert("ticketComments", {
          ticketId,
          authorId,
          visibility: comment.visibility,
          body: comment.body,
          attachments: [],
          createdAt: comment.createdAt,
          updatedAt: comment.updatedAt,
        })
        commentsInserted += 1
      }

      for (const event of ticket.events) {
        await ctx.db.insert("ticketEvents", {
          ticketId,
          type: event.type,
          payload: event.payload ?? {},
          createdAt: event.createdAt,
        })
        eventsInserted += 1
      }
    }

    return {
      usersProcessed: userCache.size,
      queuesProcessed: queueCache.size,
      ticketsUpserted,
      commentsInserted,
      eventsInserted,
    }
  },
})

export const backfillTicketCommentAuthorSnapshots = mutation({
  args: {
    limit: v.optional(v.number()),
    dryRun: v.optional(v.boolean()),
  },
  handler: async (ctx, { limit, dryRun }) => {
    const effectiveDryRun = Boolean(dryRun)
    const maxUpdates = limit && limit > 0 ? limit : null
    const comments = await ctx.db.query("ticketComments").collect()

    let updated = 0
    let skippedExisting = 0
    let missingAuthors = 0

    for (const comment of comments) {
      if (comment.authorSnapshot) {
        skippedExisting += 1
        continue
      }
      if (maxUpdates !== null && updated >= maxUpdates) {
        break
      }

      const author = await ctx.db.get(comment.authorId)
      let name: string | null = author?.name ?? null
      const email: string | null = author?.email ?? null
      let avatarUrl: string | null = author?.avatarUrl ?? null
      const teams: string[] | undefined = (author?.teams ?? undefined) as string[] | undefined

      if (!author) {
        missingAuthors += 1
        const events = await ctx.db
          .query("ticketEvents")
          .withIndex("by_ticket", (q) => q.eq("ticketId", comment.ticketId))
          .collect()
        const matchingEvent = events.find(
          (event) => event.type === "COMMENT_ADDED" && event.createdAt === comment.createdAt,
        )
        if (matchingEvent && matchingEvent.payload && typeof matchingEvent.payload === "object") {
          const payload = matchingEvent.payload as { authorName?: string; authorAvatar?: string }
          if (typeof payload.authorName === "string" && payload.authorName.trim().length > 0) {
            name = payload.authorName.trim()
          }
          if (typeof payload.authorAvatar === "string" && payload.authorAvatar.trim().length > 0) {
            avatarUrl = payload.authorAvatar
          }
        }
      }

      const snapshot = pruneUndefined({
        name: name && name.trim().length > 0 ? name : "Usuário removido",
        email: email ?? undefined,
        avatarUrl: avatarUrl ?? undefined,
        teams: teams && teams.length > 0 ? teams : undefined,
      })

      if (!effectiveDryRun) {
        await ctx.db.patch(comment._id, { authorSnapshot: snapshot })
      }
      updated += 1
    }

    return {
      dryRun: effectiveDryRun,
      totalComments: comments.length,
      updated,
      skippedExisting,
      missingAuthors,
      limit: maxUpdates,
    }
  },
})

export const syncMachineCompanyReferences = mutation({
  args: {
    tenantId: v.optional(v.string()),
    dryRun: v.optional(v.boolean()),
  },
  handler: async (ctx, { tenantId, dryRun }) => {
    const effectiveDryRun = Boolean(dryRun)

    const machines = tenantId && tenantId.trim().length > 0
      ? await ctx.db
          .query("machines")
          .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
          .collect()
      : await ctx.db.query("machines").collect()

    const slugCache = new Map<string, Id<"companies"> | null>()
    const summary = {
      total: machines.length,
      updated: 0,
      skippedMissingSlug: 0,
      skippedMissingCompany: 0,
      alreadyLinked: 0,
    }

    for (const machine of machines) {
      const slug = machine.companySlug ?? null
      if (!slug) {
        summary.skippedMissingSlug += 1
        continue
      }

      const cacheKey = `${machine.tenantId}::${slug}`
      let companyId = slugCache.get(cacheKey)
      if (companyId === undefined) {
        const company = await ctx.db
          .query("companies")
          .withIndex("by_tenant_slug", (q) => q.eq("tenantId", machine.tenantId).eq("slug", slug))
          .unique()
        companyId = company?._id ?? null
        slugCache.set(cacheKey, companyId)
      }

      if (!companyId) {
        summary.skippedMissingCompany += 1
        continue
      }

      if (machine.companyId === companyId) {
        summary.alreadyLinked += 1
        continue
      }

      if (!effectiveDryRun) {
        await ctx.db.patch(machine._id, { companyId })
      }
      summary.updated += 1
    }

    return {
      dryRun: effectiveDryRun,
      ...summary,
    }
  },
})
