import { v } from 'convex/values';

import type { MemberRole } from '../../lib/shared/schemas/organizations';
import { components } from '../_generated/api';
import { query, QueryCtx } from '../_generated/server';
import {
  getAuthUserIdentity,
  getOrganizationMember,
  getUserOrganizations,
} from '../lib/rls';
import { UnauthorizedError } from '../lib/rls/errors';
import { isAdmin } from '../lib/rls/helpers/role_helpers';
import type {
  BetterAuthFindManyResult,
  BetterAuthMember,
  BetterAuthOrganization,
  BetterAuthUser,
} from './types';
import { memberRoleValidator } from './validators';

interface BetterAuthTeam {
  _id: string;
  name: string;
  organizationId: string;
  createdAt?: number | null;
}

interface BetterAuthTeamMember {
  _id: string;
  teamId: string;
  userId: string;
  createdAt?: number | null;
}

const VALID_ROLES = new Set<string>([
  'owner',
  'disabled',
  'member',
  'editor',
  'developer',
  'admin',
]);

function isValidRole(role: string): role is MemberRole {
  return VALID_ROLES.has(role);
}

export const getCurrentMemberContext = query({
  args: { organizationId: v.string() },
  returns: v.union(
    v.object({
      status: v.literal('ok'),
      memberId: v.string(),
      organizationId: v.string(),
      userId: v.string(),
      role: memberRoleValidator,
      createdAt: v.number(),
      displayName: v.optional(v.string()),
      isAdmin: v.boolean(),
    }),
    v.object({ status: v.literal('not_found') }),
    v.object({ status: v.literal('not_member') }),
    v.null(),
  ),
  handler: async (ctx, args) => {
    const authUser = await getAuthUserIdentity(ctx);
    if (!authUser) {
      return null;
    }

    try {
      const member = await getOrganizationMember(
        ctx,
        args.organizationId,
        authUser,
      );

      const role = isValidRole(member.role) ? member.role : 'member';

      return {
        status: 'ok' as const,
        memberId: member._id,
        organizationId: member.organizationId,
        userId: member.userId,
        role,
        createdAt: member.createdAt,
        displayName: authUser.name,
        isAdmin: isAdmin(role),
      };
    } catch (error) {
      if (error instanceof UnauthorizedError) {
        try {
          const org = await ctx.runQuery(
            components.betterAuth.adapter.findOne,
            {
              model: 'organization',
              where: [
                { field: '_id', value: args.organizationId, operator: 'eq' },
              ],
            },
          );
          return org
            ? { status: 'not_member' as const }
            : { status: 'not_found' as const };
        } catch {
          return { status: 'not_found' as const };
        }
      }
      throw error;
    }
  },
});

export async function listByOrganizationHandler(
  ctx: QueryCtx,
  args: { organizationId: string },
) {
  const authUser = await getAuthUserIdentity(ctx);
  if (!authUser) {
    return [];
  }

  try {
    await getOrganizationMember(ctx, args.organizationId, authUser);
  } catch (error) {
    if (error instanceof UnauthorizedError) {
      return [];
    }
    throw error;
  }

  const result: BetterAuthFindManyResult<BetterAuthMember> = await ctx.runQuery(
    components.betterAuth.adapter.findMany,
    {
      model: 'member',
      paginationOpts: { cursor: null, numItems: 100 },
      where: [
        {
          field: 'organizationId',
          value: args.organizationId,
          operator: 'eq',
        },
      ],
    },
  );

  if (!result || result.page.length === 0) {
    return [];
  }

  // Batch-fetch every member's user record in ONE `in` query + a Map join,
  // replacing the previous findOne-per-member N+1.
  const userIds = [...new Set(result.page.map((member) => member.userId))];
  const usersById = new Map<string, BetterAuthUser>();
  if (userIds.length > 0) {
    try {
      const usersResult: BetterAuthFindManyResult<BetterAuthUser> =
        await ctx.runQuery(components.betterAuth.adapter.findMany, {
          model: 'user',
          paginationOpts: { cursor: null, numItems: userIds.length },
          where: [{ field: '_id', value: userIds, operator: 'in' }],
        });
      for (const user of usersResult?.page ?? []) {
        usersById.set(user._id, user);
      }
    } catch (error) {
      console.warn('[Members] Failed to batch-fetch user details', error);
    }
  }

  // Passkey counts ride the same batched-`in` pattern (NOT per-member N+1).
  // The count gates the admin list/revoke control in the member edit
  // dialog (#1508), so the per-member passkey query only fires for members
  // that actually have credentials.
  const passkeyCountByUserId = new Map<string, number>();
  if (userIds.length > 0) {
    try {
      let cursor: string | null = null;
      let isDone = false;
      while (!isDone) {
        const passkeysResult: BetterAuthFindManyResult<{
          _id: string;
          userId: string;
        }> = await ctx.runQuery(components.betterAuth.adapter.findMany, {
          model: 'passkey',
          paginationOpts: { cursor, numItems: 200 },
          where: [{ field: 'userId', value: userIds, operator: 'in' }],
        });
        for (const row of passkeysResult?.page ?? []) {
          passkeyCountByUserId.set(
            row.userId,
            (passkeyCountByUserId.get(row.userId) ?? 0) + 1,
          );
        }
        // Stop on `isDone` or a missing cursor — a null cursor would
        // restart from the first page and loop forever.
        isDone =
          passkeysResult?.isDone !== false || !passkeysResult.continueCursor;
        cursor = passkeysResult?.continueCursor ?? null;
      }
    } catch (error) {
      console.warn('[Members] Failed to batch-fetch passkey counts', error);
    }
  }

  return result.page.map((member) => {
    const user = usersById.get(member.userId);
    const role: MemberRole = isValidRole(member.role) ? member.role : 'member';

    return {
      _id: member._id,
      organizationId: member.organizationId,
      userId: member.userId,
      role,
      createdAt: member.createdAt,
      displayName: user?.name,
      email: user?.email,
      twoFactorEnabled: user?.twoFactorEnabled === true,
      passkeyCount: passkeyCountByUserId.get(member.userId) ?? 0,
    };
  });
}

export const listByOrganization = query({
  args: { organizationId: v.string() },
  returns: v.array(
    v.object({
      _id: v.string(),
      organizationId: v.string(),
      userId: v.string(),
      role: memberRoleValidator,
      createdAt: v.number(),
      displayName: v.optional(v.string()),
      email: v.optional(v.string()),
      twoFactorEnabled: v.boolean(),
      passkeyCount: v.number(),
    }),
  ),
  handler: listByOrganizationHandler,
});

export const getUserIdByEmail = query({
  args: { email: v.string() },
  returns: v.union(v.string(), v.null()),
  handler: async (ctx, args) => {
    const authUser = await getAuthUserIdentity(ctx);
    if (!authUser) {
      return null;
    }

    const result = await ctx.runQuery(components.betterAuth.adapter.findMany, {
      model: 'user',
      paginationOpts: { cursor: null, numItems: 1 },
      where: [{ field: 'email', value: args.email, operator: 'eq' }],
    });

    return result?.page?.[0]?._id ?? null;
  },
});

export const getUserOrganizationsList = query({
  args: {},
  returns: v.array(
    v.object({
      organizationId: v.string(),
      role: memberRoleValidator,
    }),
  ),
  handler: async (ctx) => {
    const authUser = await getAuthUserIdentity(ctx);
    if (!authUser) {
      return [];
    }

    const orgs = await getUserOrganizations(ctx, authUser);

    return orgs.map((o) => ({
      organizationId: o.organizationId,
      role: o.role,
    }));
  },
});

/**
 * Same as getUserOrganizationsList but also includes each org's name and slug.
 * Used by the login-time org picker to render a human-readable list without
 * needing N follow-up queries on the client.
 */
export const getUserOrganizationsWithDetails = query({
  args: {},
  returns: v.array(
    v.object({
      organizationId: v.string(),
      role: memberRoleValidator,
      name: v.string(),
      slug: v.optional(v.string()),
    }),
  ),
  handler: async (ctx) => {
    const authUser = await getAuthUserIdentity(ctx);
    if (!authUser) {
      return [];
    }

    const orgs = await getUserOrganizations(ctx, authUser);

    // Batch-fetch every org record in ONE `in` query + a Map join, replacing
    // the previous findOne-per-org N+1.
    const orgIds = [...new Set(orgs.map((o) => o.organizationId))];
    const orgsById = new Map<string, BetterAuthOrganization>();
    if (orgIds.length > 0) {
      try {
        const orgsResult: BetterAuthFindManyResult<BetterAuthOrganization> =
          await ctx.runQuery(components.betterAuth.adapter.findMany, {
            model: 'organization',
            paginationOpts: { cursor: null, numItems: orgIds.length },
            where: [{ field: '_id', value: orgIds, operator: 'in' }],
          });
        for (const org of orgsResult?.page ?? []) {
          orgsById.set(org._id, org);
        }
      } catch (error) {
        console.warn('[Members] Failed to batch-fetch org details', error);
      }
    }

    return orgs.map((o) => {
      const org = orgsById.get(o.organizationId);
      return {
        organizationId: o.organizationId,
        role: o.role,
        name: org?.name ?? o.organizationId,
        slug: org?.slug,
      };
    });
  },
});

export async function approxCountMyTeamsHandler(
  ctx: QueryCtx,
  args: { organizationId: string },
) {
  const authUser = await getAuthUserIdentity(ctx);
  if (!authUser) {
    return 0;
  }

  const membershipsResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
    await ctx.runQuery(components.betterAuth.adapter.findMany, {
      model: 'teamMember',
      paginationOpts: { cursor: null, numItems: 100 },
      where: [{ field: 'userId', operator: 'eq', value: authUser.userId }],
    });

  if (!membershipsResult || membershipsResult.page.length === 0) {
    return 0;
  }

  const teamResults: (BetterAuthFindManyResult<BetterAuthTeam> | null)[] =
    await Promise.all(
      membershipsResult.page.map(async (membership) => {
        try {
          return await ctx.runQuery(components.betterAuth.adapter.findMany, {
            model: 'team',
            paginationOpts: { cursor: null, numItems: 1 },
            where: [
              { field: '_id', operator: 'eq', value: membership.teamId },
              {
                field: 'organizationId',
                operator: 'eq',
                value: args.organizationId,
              },
            ],
          });
        } catch (error) {
          console.warn(
            '[Members] Failed to look up team',
            membership.teamId,
            error,
          );
          return null;
        }
      }),
    );

  return teamResults.filter((r) => r && r.page.length > 0).length;
}

export const approxCountMyTeams = query({
  args: { organizationId: v.string() },
  returns: v.number(),
  handler: approxCountMyTeamsHandler,
});

export async function getMyTeamsHandler(
  ctx: QueryCtx,
  args: { organizationId: string },
) {
  const authUser = await getAuthUserIdentity(ctx);
  if (!authUser) {
    return [];
  }

  const membershipsResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
    await ctx.runQuery(components.betterAuth.adapter.findMany, {
      model: 'teamMember',
      paginationOpts: { cursor: null, numItems: 100 },
      where: [{ field: 'userId', operator: 'eq', value: authUser.userId }],
    });

  if (!membershipsResult || membershipsResult.page.length === 0) {
    return [];
  }

  const teamIds = membershipsResult.page.map((m) => m.teamId);

  const teamResults: (BetterAuthFindManyResult<BetterAuthTeam> | null)[] =
    await Promise.all(
      teamIds.map(async (teamId) => {
        try {
          return await ctx.runQuery(components.betterAuth.adapter.findMany, {
            model: 'team',
            paginationOpts: { cursor: null, numItems: 1 },
            where: [
              { field: '_id', operator: 'eq', value: teamId },
              {
                field: 'organizationId',
                operator: 'eq',
                value: args.organizationId,
              },
            ],
          });
        } catch (error) {
          console.warn('[Members] Failed to look up team', teamId, error);
          return null;
        }
      }),
    );

  const validTeams: BetterAuthTeam[] = [];
  for (const teamResult of teamResults) {
    if (teamResult && teamResult.page.length > 0) {
      validTeams.push(teamResult.page[0]);
    }
  }

  const memberCounts = await Promise.all(
    validTeams.map(async (team) => {
      try {
        const membersResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
          await ctx.runQuery(components.betterAuth.adapter.findMany, {
            model: 'teamMember',
            paginationOpts: { cursor: null, numItems: 100 },
            where: [{ field: 'teamId', operator: 'eq', value: team._id }],
          });
        return membersResult?.page.length ?? 0;
      } catch (error) {
        console.warn('[Members] Failed to count team members', team._id, error);
        return 0;
      }
    }),
  );

  return validTeams.map((team, i) => ({
    id: team._id,
    name: team.name,
    memberCount: memberCounts[i],
    createdAt: team.createdAt ?? null,
  }));
}

export const getMyTeams = query({
  args: { organizationId: v.string() },
  returns: v.array(
    v.object({
      id: v.string(),
      name: v.string(),
      memberCount: v.number(),
      createdAt: v.union(v.number(), v.null()),
    }),
  ),
  handler: getMyTeamsHandler,
});

export async function listOrgTeamsHandler(
  ctx: QueryCtx,
  args: { organizationId: string },
) {
  const authUser = await getAuthUserIdentity(ctx);
  if (!authUser) {
    return [];
  }

  let member;
  try {
    member = await getOrganizationMember(ctx, args.organizationId, authUser);
  } catch (error) {
    if (error instanceof UnauthorizedError) {
      return [];
    }
    throw error;
  }

  if (!isAdmin(member.role)) {
    return getMyTeamsHandler(ctx, args);
  }

  const teamsResult: BetterAuthFindManyResult<BetterAuthTeam> =
    await ctx.runQuery(components.betterAuth.adapter.findMany, {
      model: 'team',
      paginationOpts: { cursor: null, numItems: 100 },
      where: [
        {
          field: 'organizationId',
          operator: 'eq',
          value: args.organizationId,
        },
      ],
    });

  if (!teamsResult || teamsResult.page.length === 0) {
    return [];
  }

  const memberCounts = await Promise.all(
    teamsResult.page.map(async (team) => {
      try {
        const membersResult: BetterAuthFindManyResult<BetterAuthTeamMember> =
          await ctx.runQuery(components.betterAuth.adapter.findMany, {
            model: 'teamMember',
            paginationOpts: { cursor: null, numItems: 100 },
            where: [{ field: 'teamId', operator: 'eq', value: team._id }],
          });
        return membersResult?.page.length ?? 0;
      } catch (error) {
        console.warn('[Members] Failed to count team members', team._id, error);
        return 0;
      }
    }),
  );

  return teamsResult.page.map((team, i) => ({
    id: team._id,
    name: team.name,
    memberCount: memberCounts[i],
    createdAt: team.createdAt ?? null,
  }));
}

export const listOrgTeams = query({
  args: { organizationId: v.string() },
  returns: v.array(
    v.object({
      id: v.string(),
      name: v.string(),
      memberCount: v.number(),
      createdAt: v.union(v.number(), v.null()),
    }),
  ),
  handler: listOrgTeamsHandler,
});
