import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from "../../../lib/database/index";
import bcrypt from "bcryptjs";
import { auth, clerkClient } from "@clerk/nextjs/server";
import { db as rawDb, sql } from "@fitai/database";
import log from "@/lib/logger";
import {
  roleFilterSchema,
  userInviteSchema,
  userUpdateWithIdSchema,
} from "@/lib/validation/schemas";
import {
  validateRequestBody,
  validateQueryParams,
  validationErrorResponse,
} from "@/lib/validation/helpers";
import {
  successResponse,
  createdResponse,
  unauthorizedResponse,
  forbiddenResponse,
  notFoundResponse,
  conflictResponse,
  internalErrorResponse,
  badRequestResponse,
} from "@/lib/api/responses";
import { getUsersByGym } from "@/lib/gym-context";
import { ensureUserSynced } from "@/lib/sync-user";

export async function GET(request: NextRequest) {
  try {
    // First authenticate the user
    const { userId: clerkUserId } = await auth();
    if (!clerkUserId) {
      return unauthorizedResponse();
    }

    const db = await getDatabase();
    const currentUser = await ensureUserSynced(clerkUserId, db);

    if (!currentUser) {
      return forbiddenResponse("User not found");
    }

    const { searchParams } = new URL(request.url);
    const role = searchParams.get("role");

    // Get target gym based on role
    const targetGymId =
      currentUser.role === "superAdmin"
        ? (searchParams.get("gymId") ?? undefined)
        : (currentUser.gymId ?? undefined);

    // Validate gym access for non-superAdmins
    if (currentUser.role !== "superAdmin" && !targetGymId) {
      return forbiddenResponse("No gym assigned");
    }

    // Get users filtered by gym
    let users = targetGymId
      ? await getUsersByGym(targetGymId)
      : await db.getAllUsers();

    // Hydrate gymId from raw DB to ensure consistency with writes
    const rawUserRows = await rawDb.all(sql`SELECT id, gym_id FROM users`);
    const gymById = new Map<string, string | null>(
      (rawUserRows || []).map((r: any) => [
        r.id as string,
        (r.gym_id as string | null) ?? null,
      ]),
    );

    // Load gym names for mapping gymId -> gymName
    const gymRows = await rawDb.all(sql`SELECT id, name FROM gyms`);
    const gymNames = new Map<string, string>(
      (gymRows || [])
        .filter((g: any) => !!g && typeof g.id === "string")
        .map((g: any) => [
          g.id as string,
          (g.name as string) || (g.id as string),
        ]),
    );
    log.debug("Fetched users from database", {
      totalUsers: Array.isArray(users) ? users.length : 0,
    });

    if (role) {
      users = users.filter((user) => user.role === role);
    }
    log.debug("Applied role filter", {
      role,
      usersAfterFilter: Array.isArray(users) ? users.length : 0,
      sample:
        users && users[0]
          ? {
              id: users[0].id,
              role: users[0].role,
              gymId: (users as any)[0].gymId,
            }
          : null,
    });

    // Batch load all related data at once to avoid N+1 queries
    const userIds = users.map((u) => u.id);
    const [allClients, allActiveCheckIns, allAttendanceHistories] =
      await Promise.all([
        db.getClientsByUserIds(userIds),
        db.getActiveCheckInsByUserIds(userIds),
        db.getAttendanceHistoriesByUserIds(userIds),
      ]);

    // Create lookup maps for O(1) access
    const clientsByUserId = new Map(allClients.map((c) => [c.userId, c]));
    const activeCheckInsByUserId = new Map(
      allActiveCheckIns.map((a) => [a.userId, a]),
    );
    // Group attendance histories by userId
    const attendanceByUserId = new Map<string, typeof allAttendanceHistories>();
    for (const attendance of allAttendanceHistories) {
      const existing = attendanceByUserId.get(attendance.userId) || [];
      existing.push(attendance);
      attendanceByUserId.set(attendance.userId, existing);
    }

    const usersWithClients = users.map((user) => {
      const { password: _, ...userWithoutPassword } = user;
      const client = clientsByUserId.get(user.id) || null;

      // Get active check-in status and statistics for ALL users
      let isCheckedIn = false;
      let checkInTime = null;
      let lastCheckInTime = null;
      let checkInsThisWeek = 0;
      let checkInsThisMonth = 0;

      // Query attendance by userId (works for all user types now)
      const activeCheckIn = activeCheckInsByUserId.get(user.id);
      if (activeCheckIn) {
        isCheckedIn = true;
        checkInTime = activeCheckIn.checkInTime;
      }

      // Get attendance history for statistics
      const attendanceHistory = attendanceByUserId.get(user.id) || [];

      if (attendanceHistory.length > 0) {
        // Last check-in is the most recent attendance record
        lastCheckInTime = attendanceHistory[0].checkInTime;

        // Calculate check-ins in last 7 days
        const weekAgo = new Date();
        weekAgo.setDate(weekAgo.getDate() - 7);
        checkInsThisWeek = attendanceHistory.filter(
          (a) => new Date(a.checkInTime) >= weekAgo,
        ).length;

        // Calculate check-ins in last 30 days
        const monthAgo = new Date();
        monthAgo.setDate(monthAgo.getDate() - 30);
        checkInsThisMonth = attendanceHistory.filter(
          (a) => new Date(a.checkInTime) >= monthAgo,
        ).length;
      }

      return {
        ...userWithoutPassword,
        // Override gymId from raw DB hydration to avoid undefined from Drizzle mapping
        gymId: gymById.get(user.id) ?? (user as any).gymId ?? undefined,
        // Provide gymName mapped from gyms table
        gymName: (() => {
          const gid = gymById.get(user.id) ?? (user as any).gymId ?? undefined;
          if (!gid) return null;
          return gymNames.get(gid) ?? null;
        })(),
        client,
        isCheckedIn,
        checkInTime,
        lastCheckInTime,
        checkInsThisWeek,
        checkInsThisMonth,
      };
    });

    log.debug("Prepared response with user details", {
      usersCount: Array.isArray(usersWithClients) ? usersWithClients.length : 0,
      sample:
        usersWithClients && usersWithClients[0]
          ? {
              id: usersWithClients[0].id,
              role: usersWithClients[0].role,
              gymId: (usersWithClients as any)[0].gymId,
            }
          : null,
    });
    return successResponse({ users: usersWithClients });
  } catch (error) {
    log.error("Failed to get users", error);
    return internalErrorResponse();
  }
}

export async function POST(request: NextRequest) {
  try {
    const { userId: clerkUserId } = await auth();
    if (!clerkUserId) {
      return unauthorizedResponse();
    }

    const db = await getDatabase();

    // Get current user to check role
    // Note: In a real app, we'd map Clerk ID to our DB ID.
    // For now, we'll assume we can find the user by some means or trust the Clerk metadata if we synced it.
    // Since we don't have Clerk ID in our local DB users table yet (we only have our own ID),
    // we might need to rely on the user being synced.
    // Let's assume the user calling this API is already in our DB.
    // For the prototype, we'll fetch the user by matching the Clerk ID if we stored it,
    // OR we'll assume the first user is Super Admin if no users exist?
    // Actually, we should look up the user by email if we can't by ID, or add a clerkId column.
    // For this step, let's assume we can get the user.

    // WAIT: The current `users` table has `id` as a string. Is it the Clerk ID?
    // In `sync-user.ts`, we use `evt.data.id` as the `id` when creating the user.
    // So yes, `users.id` IS the Clerk ID.

    const currentUser = await db.getUserById(clerkUserId);

    if (!currentUser) {
      return forbiddenResponse("Current user not found in database");
    }

    // Validate request body
    const validation = await validateRequestBody(request, userInviteSchema);

    if (!validation.success) {
      log.warn("User invitation validation failed", {
        errors: validation.errors,
      });
      return validationErrorResponse(validation.errors);
    }

    const { email, firstName, lastName, role, phone } = validation.data;

    // Enforce Hierarchy
    const allowed: Record<string, string[]> = {
      superAdmin: ["admin", "trainer", "client"],
      admin: ["trainer", "client"],
      trainer: ["client"],
      client: [],
    };

    const userRole = currentUser.role as keyof typeof allowed;
    if (!allowed[userRole] || !allowed[userRole].includes(role)) {
      return forbiddenResponse(`You are not authorized to create a ${role}`);
    }

    // Check if user already exists locally
    const existingUser = await db.getUserByEmail(email);
    if (existingUser) {
      return conflictResponse("User with this email already exists");
    }

    // Create Clerk Invitation
    // Note: We pass the role in publicMetadata so it persists when they sign up
    try {
      const client = await clerkClient();
      await client.invitations.createInvitation({
        emailAddress: email,
        publicMetadata: {
          role,
        },
        ignoreExisting: true, // Don't fail if invite exists
      });
    } catch (clerkError: any) {
      log.error("Clerk invitation failed", clerkError, {
        email,
        role,
        errorCode: clerkError.errors?.[0]?.code,
      });
      // If user already exists in Clerk, we might want to handle it.
      // But for now, let's proceed to create local record if invite sent or if they exist.
      if (clerkError.errors?.[0]?.code === "form_identifier_exists") {
        return conflictResponse("User already exists in Clerk system");
      }
      return internalErrorResponse(
        "Failed to send invitation: " + (clerkError.message || "Unknown error"),
      );
    }

    // Create user in local DB with temporary ID (will be migrated on first login)
    // We set a placeholder password since it's required by schema but won't be used
    const newUserId = await db.createUser({
      email,
      password: "INVITED_USER_PENDING",
      firstName,
      lastName,
      role,
      phone,
    });

    // If creating a client, create the client record too
    if (role === "client") {
      await db.createClient({
        userId: newUserId.id,
        membershipType: "basic",
        membershipStatus: "active",
        joinDate: new Date(),
      });
    }

    return createdResponse({
      userId: newUserId.id,
      message: "Invitation sent",
    });
  } catch (error) {
    log.error("Failed to create user", error);
    return internalErrorResponse();
  }
}

export async function PUT(request: NextRequest) {
  try {
    // Validate request body
    const validation = await validateRequestBody(
      request,
      userUpdateWithIdSchema,
    );

    if (!validation.success) {
      log.warn("User update validation failed", { errors: validation.errors });
      return validationErrorResponse(validation.errors);
    }

    const { id, email, firstName, lastName, role, phone, gymId } =
      validation.data;

    log.debug("Updating user", {
      id,
      email,
      firstName,
      lastName,
      role,
      phone,
      gymId,
    });

    const db = await getDatabase();

    // Authenticate requester
    const { userId: requesterId } = await auth();
    if (!requesterId) {
      return unauthorizedResponse();
    }

    // Fetch requester and target user
    const requester = await db.getUserById(requesterId);
    if (!requester) {
      return forbiddenResponse("Requester not found");
    }

    const existingUser = await db.getUserById(id);
    if (!existingUser) {
      return notFoundResponse("User not found");
    }

    // Authorization: determine allowed role changes
    const requesterRole = requester.role;
    const allowedByRole: Record<string, string[]> = {
      superAdmin: ["superAdmin", "admin", "trainer", "client", "generalUser"],
      admin: ["admin", "trainer", "client", "generalUser"],
      trainer: [], // trainers cannot change roles
      client: [], // clients cannot change roles
      generalUser: [], // general users cannot change roles
    };

    if (role && !allowedByRole[requesterRole]?.includes(role)) {
      return forbiddenResponse(`Not authorized to assign role '${role}'`);
    }

    // Check if email is being changed and if it's already taken
    if (email && email !== existingUser.email) {
      const userWithEmail = await db.getUserByEmail(email);
      if (userWithEmail) {
        return conflictResponse("Email already in use");
      }
    }

    // Update Clerk publicMetadata (role/gymId) to propagate via webhook
    // Note: Only update metadata when a change is requested
    try {
      const client = await clerkClient();
      const publicMetadata: Record<string, unknown> = {};
      log.debug("Preparing Clerk metadata update", {
        targetUserId: id,
        role,
        gymId,
      });

      if (role) {
        publicMetadata.role = role;
      }
      if (gymId !== undefined) {
        publicMetadata.gymId = gymId === null ? null : String(gymId);
      }

      if (Object.keys(publicMetadata).length > 0) {
        log.debug("Updating Clerk user metadata", { publicMetadata });
        const clerkResult = await client.users.updateUser(id, {
          publicMetadata,
        });
        log.debug("Clerk user updated successfully", {
          id: clerkResult.id,
          role: clerkResult.publicMetadata?.role,
          gymId: clerkResult.publicMetadata?.gymId,
        });
      } else {
        log.debug("No Clerk metadata changes requested");
      }
    } catch (clerkErr: any) {
      log.error("Clerk metadata update failed", clerkErr, { userId: id });
      return internalErrorResponse(
        "Failed to update role/gym in identity provider",
      );
    }

    // Update local DB for immediate UI feedback (webhook will also sync)
    log.debug("Updating local database user");
    await rawDb.run(
      sql`UPDATE users
          SET email = ${email ?? existingUser.email},
              first_name = ${firstName ?? existingUser.firstName},
              last_name = ${lastName ?? existingUser.lastName},
              role = ${role ?? existingUser.role},
              phone = ${phone !== undefined && typeof phone === "string" ? phone : (existingUser.phone ?? null)},
              gym_id = ${gymId !== undefined ? gymId : (existingUser.gymId ?? null)},
              updated_at = ${Date.now()}
          WHERE id = ${id}`,
    );
    // Read back the updated row to surface gym_id and confirm write
    const updatedRow = await rawDb.get(
      sql`SELECT id, email, first_name, last_name, role, phone, gym_id, created_at, updated_at FROM users WHERE id = ${id}`,
    );
    log.debug("User updated in database", { updatedRow });

    const updatedUser = {
      ...existingUser,
      email: email ?? existingUser.email,
      firstName: firstName ?? existingUser.firstName,
      lastName: lastName ?? existingUser.lastName,
      role: role ?? existingUser.role,
      phone: phone !== undefined ? phone : existingUser.phone,
      gymId:
        updatedRow && (updatedRow as any).gym_id !== undefined
          ? (updatedRow as any).gym_id
          : gymId !== undefined
            ? gymId
            : existingUser.gymId,
    };
    log.debug("Sending updated user response", { updatedUser });

    return successResponse({ user: updatedUser });
  } catch (error) {
    log.error("Failed to update user", error);
    return internalErrorResponse();
  }
}

export async function DELETE(request: NextRequest) {
  try {
    const db = await getDatabase();
    const { searchParams } = new URL(request.url);
    const id = searchParams.get("id");
    const body = await request.json().catch(() => ({}));
    const { ids } = body;

    if (ids && Array.isArray(ids)) {
      // Bulk delete
      await Promise.all(ids.map((userId: string) => db.deleteUser(userId)));
      return successResponse({ deleted: ids.length });
    } else if (id) {
      // Single delete
      const user = await db.getUserById(id);
      if (!user) {
        return notFoundResponse("User not found");
      }
      await db.deleteUser(id);
      return successResponse({ deleted: 1 });
    } else {
      return badRequestResponse("User ID or IDs array required");
    }
  } catch (error) {
    log.error("Failed to delete user(s)", error);
    return internalErrorResponse();
  }
}
