/* eslint-disable @typescript-eslint/no-explicit-any */

import { mutation, internalMutation } from "../_generated/server";
import type { Doc, Id } from "../_generated/dataModel";
import { v } from "convex/values";
import { createId } from "@paralleldrive/cuid2";
import { cascadeDelete } from "../helpers/cascade_delete";

async function findUserByStableId(
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  ctx: any,
  id: string,
): Promise<Doc<"users"> | null> {
  const byAppUserId = await ctx.db
    .query("users")
    .withIndex("by_appUserId", (q: any) => q.eq("appUserId", id))
    .unique();
  if (byAppUserId) return byAppUserId;

  const byPrismaId = await ctx.db
    .query("users")
    .filter((q: any) => q.eq(q.field("prismaId"), id))
    .first();
  if (byPrismaId) return byPrismaId;

  try {
    return await ctx.db.get(id as Id<"users">);
  } catch {
    return null;
  }
}

/**
 * Create a new user record (called during Clerk webhook or sign-up)
 * Verifies Clerk identity before inserting.
 * Phase 1: Auth & Organizations
 */
export const createUser = mutation({
  args: {
    email: v.string(),
    name: v.string(),
    clerkId: v.optional(v.string()),
    role: v.optional(v.string()),
    appUserId: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");

    const now = Date.now();
    const appUserId = args.appUserId ?? createId();
    return await ctx.db.insert("users", {
      appUserId,
      email: args.email,
      name: args.name,
      prismaId: appUserId,
      clerkId: args.clerkId ?? identity.subject,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      role: (args.role as any) ?? "CUSTOMER",
      createdAt: now,
      updatedAt: now,
      failedAttempts: 0,
      loginCount: 0,
      isActive: true,
      isSuspended: false,
      onboardingComplete: false,
      onboardingStep: 0,
      whatsappConnected: false,
    });
  },
});

/**
 * Update user profile fields
 * Verifies Clerk identity before patching.
 * Phase 1: Auth & Organizations
 */
export const updateUser = mutation({
  args: {
    id: v.id("users"),
    name: v.optional(v.string()),
    appUserId: v.optional(v.string()),
    clerkId: v.optional(v.string()),
    phoneNumber: v.optional(v.string()),
    companyName: v.optional(v.string()),
    companySize: v.optional(v.string()),
    industry: v.optional(v.string()),
    country: v.optional(v.string()),
    city: v.optional(v.string()),
    address: v.optional(v.string()),
    website: v.optional(v.string()),
    avatar: v.optional(v.string()),
    preferredCurrency: v.optional(v.string()),
    selectedPackage: v.optional(v.string()),
    selectedBillingPeriod: v.optional(
      v.union(v.literal("monthly"), v.literal("yearly")),
    ),
    goals: v.optional(v.any()),
    stripeCustomerId: v.optional(v.string()),
    password: v.optional(v.string()),
    emailVerified: v.optional(v.number()),
    failedAttempts: v.optional(v.number()),
    lockedUntil: v.optional(v.union(v.number(), v.null())),
    lastLoginAt: v.optional(v.number()),
    lastLoginIp: v.optional(v.string()),
    lastLoginCountry: v.optional(v.string()),
    lastLoginCity: v.optional(v.string()),
    loginCount: v.optional(v.number()),
    onboardingComplete: v.optional(v.boolean()),
    onboardingStep: v.optional(v.number()),
    isTrialOver: v.optional(v.boolean()),
    setupFeePaid: v.optional(v.boolean()),
    setupFeePaidAt: v.optional(v.number()),
    updatedAt: v.optional(v.number()),
    createdAt: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const { id, ...fields } = args;
    // Build patch object from provided fields only
    const updates: Record<string, unknown> = { updatedAt: Date.now() };
    for (const [key, val] of Object.entries(fields)) {
      if (val !== undefined) updates[key] = val;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await ctx.db.patch(id, updates as any);
    return await ctx.db.get(id);
  },
});

/**
 * Update user fields from server-side (no Clerk JWT available).
 * Called from API routes / webhooks. Accepts any subset of user fields.
 */
export const updateUserNoAuth = mutation({
  args: {
    userId: v.string(),
    name: v.optional(v.string()),
    appUserId: v.optional(v.string()),
    clerkId: v.optional(v.string()),
    phoneNumber: v.optional(v.string()),
    companyName: v.optional(v.string()),
    industry: v.optional(v.string()),
    companySize: v.optional(v.string()),
    country: v.optional(v.string()),
    city: v.optional(v.string()),
    address: v.optional(v.string()),
    website: v.optional(v.string()),
    avatar: v.optional(v.string()),
    preferredCurrency: v.optional(v.string()),
    selectedPackage: v.optional(v.string()),
    selectedBillingPeriod: v.optional(
      v.union(v.literal("monthly"), v.literal("yearly")),
    ),
    goals: v.optional(v.any()),
    stripeCustomerId: v.optional(v.string()),
    password: v.optional(v.string()),
    emailVerified: v.optional(v.number()),
    failedAttempts: v.optional(v.number()),
    lockedUntil: v.optional(v.union(v.number(), v.null())),
    lastLoginAt: v.optional(v.number()),
    lastLoginIp: v.optional(v.string()),
    lastLoginCountry: v.optional(v.string()),
    lastLoginCity: v.optional(v.string()),
    loginCount: v.optional(v.number()),
    onboardingComplete: v.optional(v.boolean()),
    onboardingStep: v.optional(v.number()),
    isTrialOver: v.optional(v.boolean()),
    setupFeePaid: v.optional(v.boolean()),
    setupFeePaidAt: v.optional(v.number()),
    updatedAt: v.optional(v.number()),
    createdAt: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const { userId, ...fields } = args;
    const user = await findUserByStableId(ctx, userId);
    if (!user) return null;

    const updates: Record<string, unknown> = { updatedAt: Date.now() };
    for (const [key, val] of Object.entries(fields)) {
      if (val !== undefined) updates[key] = val;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await ctx.db.patch(user._id, updates as any);
    return await ctx.db.get(user._id);
  },
});

/**
 * Cascade-delete a user and all dependent records
 * Verifies Clerk identity before deleting.
 * Phase 1: Auth & Organizations
 */
export const deleteUser = mutation({
  args: { id: v.id("users") },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");
    return await cascadeDelete(ctx, "users", args.id);
  },
});

/**
 * Cascade-delete a user from server-side code using the stable app user ID model.
 */
export const deleteUserNoAuth = internalMutation({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const user = await findUserByStableId(ctx, args.userId);
    if (!user) return null;
    return await cascadeDelete(ctx, "users", user._id);
  },
});

/**
 * Create a new session entry (tracked on login)
 * Phase 1: Auth & Organizations
 */
export const createSession = mutation({
  args: {
    userId: v.string(),
    token: v.string(),
    deviceName: v.string(),
    deviceType: v.string(),
    ipAddress: v.string(),
    userAgent: v.string(),
    expiresAt: v.number(),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");

    const now = Date.now();
    return await ctx.db.insert("user_sessions", {
      userId: args.userId,
      token: args.token,
      deviceName: args.deviceName,
      deviceType: args.deviceType,
      ipAddress: args.ipAddress,
      userAgent: args.userAgent,
      expiresAt: args.expiresAt,
      lastActive: now,
      createdAt: now,
    });
  },
});

/**
 * Delete / invalidate a session
 * Phase 1: Auth & Organizations
 */
export const deleteSession = mutation({
  args: { id: v.id("user_sessions") },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");
    await ctx.db.delete(args.id);
  },
});

/**
 * Update lastLoginAt and increment loginCount on successful sign-in
 * Phase 1: Auth & Organizations
 */
export const updateLastLogin = mutation({
  args: {
    id: v.id("users"),
    ipAddress: v.optional(v.string()),
    city: v.optional(v.string()),
    country: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");

    const user = await ctx.db.get(args.id);
    if (!user) throw new Error("User not found");

    const now = Date.now();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const patch: Record<string, any> = {
      lastLoginAt: now,
      loginCount: user.loginCount + 1,
      updatedAt: now,
    };
    if (args.ipAddress !== undefined) patch.lastLoginIp = args.ipAddress;
    if (args.city !== undefined) patch.lastLoginCity = args.city;
    if (args.country !== undefined) patch.lastLoginCountry = args.country;

    await ctx.db.patch(args.id, patch);
  },
});

/**
 * Create user from server-side (no Clerk JWT available).
 * Called from API routes / webhooks. Idempotent — skips if user exists by email.
 */
export const createUserNoAuth = internalMutation({
  args: {
    email: v.string(),
    name: v.string(),
    clerkId: v.optional(v.string()),
    prismaId: v.optional(v.string()),
    appUserId: v.optional(v.string()),
    password: v.optional(v.string()),
    selectedPackage: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();
    if (existing) {
      const patch: Record<string, unknown> = { updatedAt: Date.now() };
      if (args.clerkId && !existing.clerkId) {
        patch.clerkId = args.clerkId;
      }
      if (Object.keys(patch).length > 1) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        await ctx.db.patch(existing._id, patch as any);
      }
      return existing.appUserId ?? existing.prismaId ?? existing._id;
    }
    const now = Date.now();
    const stableAppUserId = args.appUserId ?? args.prismaId ?? createId();
    await ctx.db.insert("users", {
      appUserId: stableAppUserId,
      prismaId: args.prismaId ?? stableAppUserId,
      email: args.email,
      name: args.name,
      password: args.password,
      clerkId: args.clerkId,
      role: "CUSTOMER" as const,
      createdAt: now,
      updatedAt: now,
      failedAttempts: 0,
      loginCount: 0,
      isActive: true,
      isSuspended: false,
      onboardingComplete: false,
      onboardingStep: 0,
      selectedPackage: args.selectedPackage,
      whatsappConnected: false,
    });
    return stableAppUserId;
  },
});

/**
 * Create or replace a session from server-side code.
 * Used by OTP/password login flows without requiring a Clerk JWT.
 */
export const createSessionNoAuth = internalMutation({
  args: {
    userId: v.string(),
    token: v.string(),
    deviceName: v.string(),
    deviceType: v.string(),
    ipAddress: v.string(),
    location: v.optional(v.union(v.string(), v.null())),
    country: v.optional(v.union(v.string(), v.null())),
    city: v.optional(v.union(v.string(), v.null())),
    userAgent: v.string(),
    expiresAt: v.number(),
    loginHistoryId: v.optional(v.union(v.string(), v.null())),
    maxSessions: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const maxSessions = args.maxSessions ?? 10;
    const now = Date.now();

    const existingSessions = await ctx.db
      .query("user_sessions")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .order("desc")
      .collect();

    const expired = existingSessions.filter(
      (session) => session.expiresAt <= now,
    );
    for (const session of expired) {
      await ctx.db.delete(session._id);
    }

    const activeSessions = existingSessions.filter(
      (session) => session.expiresAt > now,
    );
    if (activeSessions.length >= maxSessions) {
      const sessionsToDelete = activeSessions.slice(maxSessions - 1);
      for (const session of sessionsToDelete) {
        await ctx.db.delete(session._id);
      }
    }

    return await ctx.db.insert("user_sessions", {
      userId: args.userId,
      token: args.token,
      deviceName: args.deviceName,
      deviceType: args.deviceType,
      ipAddress: args.ipAddress,
      location: args.location ?? undefined,
      country: args.country ?? undefined,
      city: args.city ?? undefined,
      userAgent: args.userAgent,
      lastActive: now,
      createdAt: now,
      expiresAt: args.expiresAt,
      loginHistoryId: args.loginHistoryId ?? undefined,
    });
  },
});

/**
 * Delete a session from server-side code using a stable string identifier.
 */
export const deleteSessionNoAuth = internalMutation({
  args: { sessionId: v.string() },
  handler: async (ctx, args) => {
    let session = await ctx.db
      .query("user_sessions")
      .filter((q) => q.eq(q.field("prismaId"), args.sessionId))
      .first();

    if (!session) {
      try {
        session = await ctx.db.get(args.sessionId as Id<"user_sessions">);
      } catch {
        session = null;
      }
    }

    if (!session) return null;

    await ctx.db.delete(session._id);
    return session._id;
  },
});

/**
 * Revoke all sessions for a user except the current one.
 */
export const revokeOtherSessionsNoAuth = internalMutation({
  args: {
    userId: v.string(),
    currentToken: v.string(),
  },
  handler: async (ctx, args) => {
    const sessions = await ctx.db
      .query("user_sessions")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .collect();

    let revokedCount = 0;
    for (const session of sessions) {
      if (session.token === args.currentToken) continue;
      await ctx.db.delete(session._id);
      revokedCount += 1;
    }

    return revokedCount;
  },
});

/**
 * Touch a session when it is used successfully.
 */
export const touchSessionByToken = internalMutation({
  args: { token: v.string() },
  handler: async (ctx, args) => {
    const session = await ctx.db
      .query("user_sessions")
      .withIndex("by_token", (q) => q.eq("token", args.token))
      .unique();

    if (!session) return null;

    await ctx.db.patch(session._id, {
      lastActive: Date.now(),
    });
    return session._id;
  },
});

/**
 * Mark a login history record as logged out.
 */
export const markLoginHistoryLoggedOut = internalMutation({
  args: { loginHistoryId: v.string() },
  handler: async (ctx, args) => {
    let record = await ctx.db
      .query("login_history")
      .filter((q) => q.eq(q.field("prismaId"), args.loginHistoryId))
      .first();

    if (!record) {
      try {
        record = await ctx.db.get(args.loginHistoryId as Id<"login_history">);
      } catch {
        record = null;
      }
    }

    if (!record) return null;

    await ctx.db.patch(record._id, {
      logoutAt: Date.now(),
    });
    return record._id;
  },
});

/**
 * Mark user as onboarding-complete from server-side (no Clerk JWT available).
 * Called from /api/signup/success after Stripe payment.
 */
export const setOnboardingCompleteNoAuth = internalMutation({
  args: {
    email: v.string(),
    onboardingComplete: v.boolean(),
    trialEndsAt: v.union(v.number(), v.null()),
  },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();
    if (!user) return null;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const patch: Record<string, unknown> = {
      onboardingComplete: args.onboardingComplete,
      updatedAt: Date.now(),
    };
    if (args.trialEndsAt !== null) patch.trialEndsAt = args.trialEndsAt;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await ctx.db.patch(user._id, patch as any);
    return user._id;
  },
});

// ── WU-17 gap-fill mutations ────────────────────────────────────────────

/**
 * Update or create user preferences
 * WU-17: User preferences data-source routing
 */
export const updatePreferences = internalMutation({
  args: {
    userId: v.string(),
    language: v.optional(v.string()),
    timezone: v.optional(v.string()),
    dateFormat: v.optional(v.string()),
    emailNotifications: v.optional(v.any()),
    emailFrequency: v.optional(v.string()),
    dashboardPreferences: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const { userId, ...fields } = args;
    const existing = await ctx.db
      .query("user_preferences")
      .withIndex("by_userId", (q) => q.eq("userId", userId))
      .unique();

    const now = Date.now();
    const updates: Record<string, unknown> = { updatedAt: now };
    for (const [key, val] of Object.entries(fields)) {
      if (val !== undefined) updates[key] = val;
    }

    if (existing) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await ctx.db.patch(existing._id, updates as any);
      return existing._id;
    } else {
      // Create with defaults
      return await ctx.db.insert("user_preferences", {
        userId,
        language: fields.language ?? "ar",
        timezone: fields.timezone ?? "Asia/Qatar",
        dateFormat: fields.dateFormat ?? "DD/MM/YYYY",
        theme: "system",
        emailNotifications: fields.emailNotifications ?? {
          bookings: true,
          reminders: true,
          payments: true,
          marketing: false,
          security: true,
          weeklyReport: false,
        },
        smsNotifications: {},
        whatsappNotifications: {},
        emailFrequency: fields.emailFrequency ?? "instant",
        dashboardPreferences: fields.dashboardPreferences ?? {
          defaultTab: "overview",
          refreshInterval: 60,
          viewDensity: "comfortable",
        },
        createdAt: now,
        updatedAt: now,
      });
    }
  },
});

/**
 * Update password hash for a user looked up by email (server-side, no Clerk JWT)
 * WU-17: Password change mirror
 */
export const changePasswordByEmail = internalMutation({
  args: {
    email: v.string(),
    passwordHash: v.string(),
  },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();
    if (!user) return null;
    await ctx.db.patch(user._id, {
      password: args.passwordHash,
      failedAttempts: 0,
      updatedAt: Date.now(),
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } as any);
    return user._id;
  },
});

/**
 * Update a user's status (activate/deactivate/suspend)
 * Used by staff admin panel — no Clerk auth required (server-side only).
 * WU-15: Staff user management
 */
export const updateUserStatus = internalMutation({
  args: {
    userId: v.string(),
    isSuspended: v.boolean(),
    isActive: v.boolean(),
    suspendedAt: v.optional(v.number()),
    suspendedBy: v.optional(v.string()),
    suspendedReason: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // Find user by prismaId or email
    const user = await ctx.db
      .query("users")
      .filter((q) => q.eq(q.field("prismaId"), args.userId))
      .first();
    if (!user) return null;

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const patch: Record<string, any> = {
      isSuspended: args.isSuspended,
      isActive: args.isActive,
      updatedAt: Date.now(),
    };
    if (args.suspendedAt !== undefined) patch.suspendedAt = args.suspendedAt;
    if (args.suspendedBy !== undefined) patch.suspendedBy = args.suspendedBy;
    if (args.suspendedReason !== undefined)
      patch.suspendedReason = args.suspendedReason;

    await ctx.db.patch(user._id, patch);
    return user._id;
  },
});

/**
 * Insert an audit log entry (server-side, no Clerk JWT)
 * WU-17: Audit log data-source routing
 */
export const createAuditLog = internalMutation({
  args: {
    userId: v.optional(v.string()),
    type: v.string(),
    action: v.string(),
    success: v.boolean(),
    ip: v.string(),
    userAgent: v.optional(v.string()),
    metadata: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("audit_logs", {
      userId: args.userId,
      type: args.type,
      action: args.action,
      success: args.success,
      ip: args.ip,
      userAgent: args.userAgent,
      metadata: args.metadata,
      createdAt: Date.now(),
    });
  },
});

/**
 * Update onboarding progress for a specific step
 * WU-17: Onboarding progress data-source routing
 */
export const updateOnboardingProgress = internalMutation({
  args: {
    userId: v.string(),
    step: v.string(),
    completed: v.boolean(),
    whatsappMethod: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("onboarding_progress")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .unique();

    const now = Date.now();
    const stepField = `${args.step}Completed` as string;

    if (existing) {
      const patch: Record<string, unknown> = {
        [stepField]: args.completed,
        lastStepAt: now,
      };
      if (args.whatsappMethod) patch.whatsappMethod = args.whatsappMethod;
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await ctx.db.patch(existing._id, patch as any);
      return existing._id;
    } else {
      return await ctx.db.insert("onboarding_progress", {
        userId: args.userId,
        welcomeCompleted: args.step === "welcome" ? args.completed : false,
        whatsappCompleted: args.step === "whatsapp" ? args.completed : false,
        whatsappMethod: args.whatsappMethod,
        voiceAgentCompleted:
          args.step === "voiceAgent" ? args.completed : false,
        knowledgeBaseCompleted:
          args.step === "knowledgeBase" ? args.completed : false,
        workflowsCompleted: args.step === "workflows" ? args.completed : false,
        testingCompleted: args.step === "testing" ? args.completed : false,
        startedAt: now,
        lastStepAt: now,
        dismissCount: 0,
      });
    }
  },
});

/**
 * Dismiss onboarding (sets dismissedAt, increments dismissCount)
 * WU-17: Onboarding dismiss data-source routing
 */
export const dismissOnboarding = internalMutation({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("onboarding_progress")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .unique();

    const now = Date.now();
    if (existing) {
      await ctx.db.patch(existing._id, {
        dismissedAt: now,
        dismissCount: existing.dismissCount + 1,
        lastStepAt: now,
      });
      return existing._id;
    } else {
      return await ctx.db.insert("onboarding_progress", {
        userId: args.userId,
        welcomeCompleted: false,
        whatsappCompleted: false,
        voiceAgentCompleted: false,
        knowledgeBaseCompleted: false,
        workflowsCompleted: false,
        testingCompleted: false,
        startedAt: now,
        lastStepAt: now,
        dismissedAt: now,
        dismissCount: 1,
      });
    }
  },
});

/**
 * Mark onboarding as complete (sets completedAt on progress + users.onboardingComplete)
 * WU-17: Onboarding complete data-source routing
 */
export const completeOnboarding = internalMutation({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const now = Date.now();

    // Update onboarding_progress
    const progress = await ctx.db
      .query("onboarding_progress")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .unique();

    if (progress) {
      await ctx.db.patch(progress._id, {
        completedAt: now,
        lastStepAt: now,
      });
    }

    // Also update users.onboardingComplete
    const user = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.userId))
      .first();

    if (!user) {
      // Try prismaId approach — userId might be Prisma ID
      const allUsers = await ctx.db.query("users").collect();
      const matched = allUsers.find((u) => u.prismaId === args.userId);
      if (matched) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        await ctx.db.patch(matched._id, {
          onboardingComplete: true,
          updatedAt: now,
        } as any);
      }
    } else {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await ctx.db.patch(user._id, {
        onboardingComplete: true,
        updatedAt: now,
      } as any);
    }

    return progress?._id ?? null;
  },
});

/**
 * Add an admin note to a user account
 * Used by staff admin panel — no Clerk auth required (server-side only).
 * WU-15: Staff user management
 */
export const addUserNote = internalMutation({
  args: {
    userId: v.string(),
    staffId: v.string(),
    content: v.string(),
    category: v.string(),
    isPrivate: v.boolean(),
  },
  handler: async (ctx, args) => {
    const now = Date.now();
    const id = await ctx.db.insert("admin_notes", {
      userId: args.userId,
      staffId: args.staffId,
      content: args.content,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      category: args.category as any,
      isPrivate: args.isPrivate,
      createdAt: now,
      updatedAt: now,
    });
    return id;
  },
});

/**
 * Insert a login_history record (called on every login attempt, success or failure)
 * No auth check required — must be callable for failed logins too.
 * Phase 1: Auth & Organizations
 */
export const createLoginHistory = internalMutation({
  args: {
    userId: v.string(),
    ipAddress: v.string(),
    provider: v.string(),
    success: v.boolean(),
    userAgent: v.optional(v.string()),
    failReason: v.optional(v.string()),
    country: v.optional(v.string()),
    countryCode: v.optional(v.string()),
    city: v.optional(v.string()),
    region: v.optional(v.string()),
    latitude: v.optional(v.number()),
    longitude: v.optional(v.number()),
    timezone: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("login_history", {
      userId: args.userId,
      ipAddress: args.ipAddress,
      provider: args.provider,
      success: args.success,
      userAgent: args.userAgent,
      failReason: args.failReason,
      country: args.country,
      countryCode: args.countryCode,
      city: args.city,
      region: args.region,
      latitude: args.latitude,
      longitude: args.longitude,
      timezone: args.timezone,
      createdAt: Date.now(),
    });
  },
});

// ── User Preferences Mutations ───────────────────────────────────────

/**
 * Create or update user preferences (upsert by userId)
 */
export const upsertUserPreferences = mutation({
  args: {
    userId: v.string(),
    language: v.optional(v.string()),
    timezone: v.optional(v.string()),
    dateFormat: v.optional(v.string()),
    emailNotifications: v.optional(v.any()),
    emailFrequency: v.optional(v.string()),
    dashboardPreferences: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("user_preferences")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .unique();

    const now = Date.now();
    const { userId, ...fields } = args;

    if (existing) {
      // Update existing
      const updates: Record<string, unknown> = { updatedAt: now };
      for (const [key, val] of Object.entries(fields)) {
        if (val !== undefined) updates[key] = val;
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      await ctx.db.patch(existing._id, updates as any);
      return await ctx.db.get(existing._id);
    } else {
      // Create with defaults
      const id = await ctx.db.insert("user_preferences", {
        userId,
        language: fields.language ?? "ar",
        timezone: fields.timezone ?? "Asia/Qatar",
        dateFormat: fields.dateFormat ?? "DD/MM/YYYY",
        theme: "system",
        emailNotifications: fields.emailNotifications ?? {
          bookings: true,
          reminders: true,
          payments: true,
          marketing: false,
          security: true,
          weeklyReport: false,
        },
        smsNotifications: {},
        whatsappNotifications: {},
        emailFrequency: fields.emailFrequency ?? "instant",
        dashboardPreferences: fields.dashboardPreferences ?? {
          defaultTab: "overview",
          refreshInterval: 60,
          viewDensity: "comfortable",
        },
        createdAt: now,
        updatedAt: now,
      });
      return await ctx.db.get(id);
    }
  },
});

// ── Audit Log Mutations ──────────────────────────────────────────────

/**
 * Create an audit log entry (public — callable from HTTP client)
 * No auth check required — must be callable from server-side API routes.
 */
export const createAuditLogPublic = mutation({
  args: {
    userId: v.optional(v.string()),
    type: v.string(),
    action: v.string(),
    success: v.boolean(),
    ip: v.string(),
    userAgent: v.optional(v.string()),
    metadata: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    return await ctx.db.insert("audit_logs", {
      userId: args.userId,
      type: args.type,
      action: args.action,
      success: args.success,
      ip: args.ip,
      userAgent: args.userAgent,
      metadata: args.metadata,
      createdAt: Date.now(),
    });
  },
});

// ── OTP Mutations ───────────────────────────────────────────────────

/**
 * Create a new OTP record (replaces Redis setex for OTP storage)
 * No auth check required — must be callable during signup/login flows.
 */
export const createOtp = mutation({
  args: {
    email: v.string(),
    otpCode: v.string(),
    purpose: v.union(
      v.literal("SIGNUP"),
      v.literal("LOGIN"),
      v.literal("PASSWORD_RESET"),
    ),
    expiresAt: v.number(),
    ipAddress: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    // Delete any existing OTP for this email (one active OTP per email)
    const existing = await ctx.db
      .query("otps")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .collect();
    for (const otp of existing) {
      await ctx.db.delete(otp._id);
    }

    const now = Date.now();
    return await ctx.db.insert("otps", {
      email: args.email,
      otp_code: args.otpCode,
      purpose: args.purpose,
      expires_at: args.expiresAt,
      verified: false,
      ip_address: args.ipAddress,
      created_at: now,
      updated_at: now,
    });
  },
});

/**
 * Verify and consume an OTP (one-time use)
 * Returns true if the OTP is valid, false otherwise.
 * Deletes the OTP on successful verification.
 */
export const verifyOtp = mutation({
  args: {
    email: v.string(),
    otpCode: v.string(),
  },
  handler: async (ctx, args) => {
    const otp = await ctx.db
      .query("otps")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .order("desc")
      .first();

    if (!otp) return { valid: false, reason: "not_found" };
    if (otp.expires_at < Date.now()) {
      await ctx.db.delete(otp._id);
      return { valid: false, reason: "expired" };
    }
    if (otp.verified) {
      return { valid: false, reason: "already_used" };
    }

    // The caller passes the bcrypt hash comparison result
    // We store the hash in otp_code, so just return the hash for comparison
    return { valid: true, storedHash: otp.otp_code, otpId: otp._id };
  },
});

/**
 * Delete OTP after successful verification or manual cleanup
 */
export const deleteOtp = mutation({
  args: {
    email: v.string(),
  },
  handler: async (ctx, args) => {
    const otps = await ctx.db
      .query("otps")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .collect();
    for (const otp of otps) {
      await ctx.db.delete(otp._id);
    }
  },
});

/**
 * Mark OTP as verified (consumed)
 */
export const markOtpVerified = mutation({
  args: {
    otpId: v.id("otps"),
  },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.otpId, {
      verified: true,
      verified_at: Date.now(),
      updated_at: Date.now(),
    });
  },
});

// ── Secure Token Mutations ──────────────────────────────────────────

/**
 * Create a password reset token (replaces Redis setex for reset tokens)
 * No auth check — must work for unauthenticated password reset flows.
 */
export const createResetToken = mutation({
  args: {
    email: v.string(),
    token: v.string(),
    expiresAt: v.number(),
  },
  handler: async (ctx, args) => {
    // Delete any existing reset tokens for this email
    const existing = await ctx.db
      .query("otps")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .collect();
    for (const record of existing) {
      if (record.purpose === "PASSWORD_RESET") {
        await ctx.db.delete(record._id);
      }
    }

    const now = Date.now();
    return await ctx.db.insert("otps", {
      email: args.email,
      otp_code: args.token,
      purpose: "PASSWORD_RESET",
      expires_at: args.expiresAt,
      verified: false,
      created_at: now,
      updated_at: now,
    });
  },
});

/**
 * Consume a reset token — returns the stored token if valid, then deletes it
 */
export const consumeResetToken = mutation({
  args: {
    email: v.string(),
  },
  handler: async (ctx, args) => {
    const record = await ctx.db
      .query("otps")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .order("desc")
      .first();

    if (!record || record.purpose !== "PASSWORD_RESET") {
      return null;
    }

    if (record.expires_at < Date.now()) {
      await ctx.db.delete(record._id);
      return null;
    }

    // Delete the token (one-time use)
    await ctx.db.delete(record._id);
    return { token: record.otp_code, email: record.email };
  },
});

// ── Onboarding Progress Mutations ────────────────────────────────────

/**
 * Create initial onboarding progress record
 */
export const createOnboardingProgress = mutation({
  args: { userId: v.string() },
  handler: async (ctx, args) => {
    const now = Date.now();
    const id = await ctx.db.insert("onboarding_progress", {
      userId: args.userId,
      welcomeCompleted: false,
      whatsappCompleted: false,
      voiceAgentCompleted: false,
      knowledgeBaseCompleted: false,
      workflowsCompleted: false,
      testingCompleted: false,
      startedAt: now,
      lastStepAt: now,
      dismissCount: 0,
    });
    return await ctx.db.get(id);
  },
});

/**
 * Update onboarding progress step (public — callable from HTTP client)
 */
export const updateOnboardingProgressPublic = mutation({
  args: {
    userId: v.string(),
    welcomeCompleted: v.optional(v.boolean()),
    whatsappCompleted: v.optional(v.boolean()),
    whatsappMethod: v.optional(v.string()),
    voiceAgentCompleted: v.optional(v.boolean()),
    knowledgeBaseCompleted: v.optional(v.boolean()),
    workflowsCompleted: v.optional(v.boolean()),
    testingCompleted: v.optional(v.boolean()),
    completedAt: v.optional(v.number()),
    dismissedAt: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("onboarding_progress")
      .withIndex("by_userId", (q) => q.eq("userId", args.userId))
      .unique();

    if (!existing) {
      throw new Error("Onboarding progress not found for user");
    }

    const updates: Record<string, unknown> = { lastStepAt: Date.now() };
    for (const [key, val] of Object.entries(args)) {
      if (key !== "userId" && val !== undefined) updates[key] = val;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await ctx.db.patch(existing._id, updates as any);
    return await ctx.db.get(existing._id);
  },
});
