/**
 * Users domain queries
 *
 * Public query operations for users.
 */

import { v, type Infer } from 'convex/values';

import { getString, isRecord } from '../../lib/utils/type-guards';
import { components } from '../_generated/api';
import { query, type QueryCtx } from '../_generated/server';
import { getStrictestPasswordPolicyForUser } from '../governance/helpers';
import { getAuthUserIdentity, type AuthenticatedUser } from '../lib/rls';
import { getUserOrganizations } from '../lib/rls/organization/get_user_organizations';
import { hasAnyUsers as hasAnyUsersHelper } from './has_any_users';

/**
 * Check if any users exist in the system.
 * Used to determine if this is a fresh installation that should redirect to sign-up.
 */
export const hasAnyUsers = query({
  args: {},
  returns: v.boolean(),
  handler: async (ctx): Promise<boolean> => {
    return await hasAnyUsersHelper(ctx);
  },
});

/**
 * Get the current authenticated user.
 * Returns null if not authenticated.
 *
 * Reads `name` and `email` from the Better Auth `user` row, not the JWT
 * identity, so profile updates surface immediately without forcing the user
 * to sign out and back in. The JWT identity is only used to resolve the
 * caller's userId (auth gate).
 */
export const getCurrentUser = query({
  args: {},
  returns: v.union(
    v.object({
      userId: v.string(),
      email: v.optional(v.string()),
      name: v.optional(v.string()),
    }),
    v.null(),
  ),
  handler: async (
    ctx,
  ): Promise<{
    userId: string;
    email?: string;
    name?: string;
  } | null> => {
    const identity = await getAuthUserIdentity(ctx);
    if (!identity) return null;

    const userRow = await ctx.runQuery(components.betterAuth.adapter.findOne, {
      model: 'user',
      where: [{ field: '_id', value: identity.userId, operator: 'eq' }],
    });

    const fresh = isRecord(userRow) ? userRow : undefined;
    const name = fresh
      ? (getString(fresh, 'name') ?? identity.name)
      : identity.name;
    const email = fresh
      ? (getString(fresh, 'email') ?? identity.email)
      : identity.email;

    return {
      userId: identity.userId,
      email,
      name,
    };
  },
});

const DAY_MS = 24 * 60 * 60 * 1000;

/**
 * Validator for the password-expiry payload. Exported so the consolidated
 * `getAccountBootstrap` query can reuse the exact same shape.
 */
export const passwordExpiryValidator = v.object({
  expired: v.boolean(),
  daysUntilExpiry: v.union(v.number(), v.null()),
  hasCredential: v.boolean(),
  rotationEnabled: v.boolean(),
  reason: v.union(v.literal('admin_set'), v.literal('rotation'), v.null()),
});

export type PasswordExpiryStatus = Infer<typeof passwordExpiryValidator>;

/**
 * Compute password-expiry status for an already-resolved authenticated user
 * (or the not-expired default when `authUser` is null). Extracted from
 * `getPasswordExpiryStatus` so the consolidated `getAccountBootstrap` query
 * can return it inside the same Convex transaction.
 *
 * Returns `{ expired: false, daysUntilExpiry: null }` when:
 *  - the user has no credential account (OAuth-only)
 *  - no org's password_policy has rotation enabled (rotationDays === 0)
 *
 * Otherwise `daysUntilExpiry` is signed (negative when expired).
 *
 * Multi-org: the strictest policy across the user's memberships applies
 * (shortest positive rotation window).
 */
export async function computePasswordExpiry(
  ctx: QueryCtx,
  authUser: AuthenticatedUser | null,
): Promise<PasswordExpiryStatus> {
  if (!authUser) {
    return {
      expired: false,
      daysUntilExpiry: null,
      hasCredential: false,
      rotationEnabled: false,
      reason: null,
    };
  }

  const credentialResult = await ctx.runQuery(
    components.betterAuth.adapter.findMany,
    {
      model: 'account',
      where: [
        { field: 'userId', value: authUser.userId, operator: 'eq' },
        { field: 'providerId', value: 'credential', operator: 'eq' },
      ],
      paginationOpts: { cursor: null, numItems: 1 },
    },
  );
  const hasCredential = (credentialResult?.page?.length ?? 0) > 0;
  if (!hasCredential) {
    return {
      expired: false,
      daysUntilExpiry: null,
      hasCredential: false,
      rotationEnabled: false,
      reason: null,
    };
  }

  const meta = await ctx.db
    .query('userPasswordMetadata')
    .withIndex('by_userId', (q) => q.eq('userId', authUser.userId))
    .first();

  if (meta?.forceChangeOnNextLogin) {
    return {
      expired: true,
      daysUntilExpiry: 0,
      hasCredential: true,
      rotationEnabled: false,
      reason: 'admin_set',
    };
  }

  const orgs = await getUserOrganizations(ctx, authUser);
  const { policy, effectiveAt } = await getStrictestPasswordPolicyForUser(
    ctx,
    orgs.map((o) => o.organizationId),
  );

  if (policy.rotationDays <= 0) {
    return {
      expired: false,
      daysUntilExpiry: null,
      hasCredential: true,
      rotationEnabled: false,
      reason: null,
    };
  }

  const anchor = Math.max(meta?.passwordChangedAt ?? 0, effectiveAt ?? 0);

  // No anchor yet (policy never activated rotation and user never had
  // their password change timestamped). Treat as not-expired — the
  // next password change will seed passwordChangedAt.
  if (anchor === 0) {
    return {
      expired: false,
      daysUntilExpiry: null,
      hasCredential: true,
      rotationEnabled: true,
      reason: null,
    };
  }

  const expiresAt = anchor + policy.rotationDays * DAY_MS;
  const now = Date.now();
  const daysUntilExpiry = Math.ceil((expiresAt - now) / DAY_MS);
  const expired = now >= expiresAt;
  return {
    expired,
    daysUntilExpiry,
    hasCredential: true,
    rotationEnabled: true,
    reason: expired ? 'rotation' : null,
  };
}

/**
 * Password expiry status for the current authenticated user. Thin wrapper
 * over {@link computePasswordExpiry}.
 */
export const getPasswordExpiryStatus = query({
  args: {},
  returns: passwordExpiryValidator,
  handler: async (ctx) =>
    computePasswordExpiry(ctx, await getAuthUserIdentity(ctx)),
});
