import { ConvexError, v } from 'convex/values';
import { getAuthUserId } from '@convex-dev/auth/server';
import {
  type MutationCtx,
  type QueryCtx,
  action,
  mutation,
  query,
} from './_generated/server';
import { api } from './_generated/api';
import type { Doc, Id } from './_generated/dataModel';
import { paginationOptsValidator } from 'convex/server';

type RWCtx = MutationCtx | QueryCtx;
type StatusRow = {
  user: {
    id: Id<'users'>;
    email: string | null;
    name: string | null;
    imageUrl: string | null;
  };
  status: {
    id: Id<'statuses'>;
    message: string;
    updatedAt: number;
    updatedBy: StatusRow['user'] | null;
  } | null;
};

type Paginated<T> = {
  page: T[];
  isDone: boolean;
  continueCursor: string | null;
};

const ensureUser = async (ctx: RWCtx, userId: Id<'users'>) => {
  const user = await ctx.db.get(userId);
  if (!user) throw new ConvexError('User not found.');
  return user;
};

const getName = (u: Doc<'users'>): string | null =>
  'name' in u && typeof u.name === 'string' ? u.name : null;

const getEmail = (u: Doc<'users'>): string | null =>
  'email' in u && typeof u.email === 'string' ? u.email : null;

const getImageId = (u: Doc<'users'>): Id<'_storage'> | null => {
  if (!('image' in u)) return null;
  const img = (u as { image?: unknown }).image as string | undefined;
  return img && img.length > 0 ? (img as Id<'_storage'>) : null;
};

export const create = mutation({
  args: {
    message: v.string(),
    userId: v.optional(v.id('users')),
    updatedBy: v.optional(v.id('users')),
  },
  handler: async (ctx, args) => {
    const authUserId: Id<'users'> | null = await getAuthUserId(ctx);
    if (!args.userId && !authUserId) {
      throw new ConvexError('Not authenticated.');
    }
    const message = args.message.trim();
    if (message.length === 0) {
      throw new ConvexError('Message cannot be empty.');
    }
    const userId = args.userId ?? authUserId!;
    const updatedBy = args.updatedBy ?? authUserId;
    await ensureUser(ctx, userId);
    let statusId: Id<'statuses'>;
    if (updatedBy) {
      ensureUser(ctx, updatedBy);
      statusId = await ctx.db.insert('statuses', {
        message,
        userId,
        updatedBy,
        updatedAt: Date.now(),
      });
    } else {
      statusId = await ctx.db.insert('statuses', {
        message,
        userId,
        updatedAt: Date.now(),
      });
    }
    await ctx.db.patch(userId, { currentStatusId: statusId });
    return { statusId };
  },
});

export const bulkCreate = mutation({
  args: {
    message: v.string(),
    userIds: v.array(v.id('users')),
    updatedBy: v.optional(v.id('users')),
  },
  handler: async (ctx, args) => {
    const authUserId = await getAuthUserId(ctx);
    if (!authUserId) throw new ConvexError('Not authenticated.');

    if (args.userIds.length === 0) return { statusIds: [] };

    const updatedBy = args.updatedBy ?? authUserId;
    await ensureUser(ctx, updatedBy);

    const message = args.message.trim();
    if (message.length === 0) {
      throw new ConvexError('Message cannot be empty.');
    }

    const statusIds: Id<'statuses'>[] = [];
    const now = Date.now();

    for (const userId of args.userIds) {
      await ensureUser(ctx, userId);
      const statusId = await ctx.db.insert('statuses', {
        message,
        userId,
        updatedBy,
        updatedAt: now,
      });
      await ctx.db.patch(userId, { currentStatusId: statusId });
      statusIds.push(statusId);
    }

    return { statusIds };
  },
});

export const updateAllStatuses = mutation({
  args: { message: v.string() },
  handler: async (ctx, args) => {
    const users = await ctx.db.query('users').collect();
    const message = args.message.trim();
    if (message.length === 0) {
      throw new ConvexError('Message cannot be empty.');
    }

    const statusIds: Id<'statuses'>[] = [];
    const now = Date.now();

    for (const user of users) {
      const statusId = await ctx.db.insert('statuses', {
        message,
        userId: user._id,
        updatedAt: now,
      });
      await ctx.db.patch(user._id, { currentStatusId: statusId });
      statusIds.push(statusId);
    }

    return { statusIds };
  },
});

export const createLunchStatus = mutation({
  args: { userId: v.optional(v.id('users'))},
  handler: async (ctx, args) => {
    const authUserId = await getAuthUserId(ctx);
    const lunchUserId = args.userId ?? authUserId
    if (!lunchUserId) throw new ConvexError('Not authenticated.');
    await ctx.runMutation(api.statuses.create, {
      message: 'At lunch',
      userId: lunchUserId,
    });
    const oneHour = 60 * 60 * 1000;
    await ctx.scheduler.runAfter(oneHour, api.statuses.backFromLunchStatus, {
      userId: lunchUserId,
    });
    return { success: true };
  },
});

export const backFromLunchStatus = mutation({
  args: { userId: v.optional(v.id('users')) },
  handler: async (ctx, args) => {
    const authUserId = await getAuthUserId(ctx);
    const lunchUserId = args.userId ?? authUserId
    if (!lunchUserId) throw new ConvexError('Not authenticated.');
    const user = await ensureUser(ctx, lunchUserId);
    if (!user.currentStatusId) throw new ConvexError('User has no current status.');
    const currentStatus = await ctx.db.get(user.currentStatusId);
    if (currentStatus?.message === 'At lunch') {
      await ctx.runMutation(api.statuses.create, {
        message: 'At desk',
        userId: lunchUserId,
      });
    }
  },
});

export const getCurrentForUser = query({
  args: { userId: v.id('users') },
  handler: async (ctx, { userId }) => {
    const user = await ensureUser(ctx, userId);
    if (user.currentStatusId) {
      const status = await ctx.db.get(user.currentStatusId);
      if (status) return status;
    }
    const [latest] = await ctx.db
      .query('statuses')
      .withIndex('by_user_updatedAt', (q) => q.eq('userId', userId))
      .order('desc')
      .take(1);

    return latest ?? null;
  },
});

export const getCurrentForAll = query({
  args: {},
  handler: async (ctx): Promise<StatusRow[]> => {
    const users = await ctx.db.query('users').collect();
    return await Promise.all(
      users.map(async (u) => {
        let curStatus: Doc<'statuses'> | null = null;
        if ('currentStatusId' in u && u.currentStatusId) {
          curStatus = await ctx.db.get(u.currentStatusId);
        }
        if (!curStatus) {
          const [latest] = await ctx.db
            .query('statuses')
            .withIndex('by_user_updatedAt', (q) => q.eq('userId', u._id))
            .order('desc')
            .take(1);
          curStatus = latest ?? null;
        }
        const userImageId = getImageId(u);
        const userImageUrl = userImageId
          ? await ctx.storage.getUrl(userImageId)
          : null;
        let updatedByUser: StatusRow['user'] | null = null;
        if (curStatus && curStatus.updatedBy && curStatus.updatedBy !== u._id) {
          const updater = await ctx.db.get(curStatus.updatedBy);
          if (updater) {
            const updaterImageId = getImageId(updater);
            const updaterImageUrl = updaterImageId
              ? await ctx.storage.getUrl(updaterImageId)
              : null;
            updatedByUser = {
              id: updater._id,
              email: getEmail(updater),
              name: getName(updater),
              imageUrl: updaterImageUrl,
            };
          }
        }
        const status: StatusRow['status'] = curStatus
          ? {
              id: curStatus._id,
              message: curStatus.message,
              updatedAt: curStatus.updatedAt,
              updatedBy: updatedByUser,
            }
          : null;
        return {
          user: {
            id: u._id,
            email: getEmail(u),
            name: getName(u),
            imageUrl: userImageUrl,
          },
          status,
        };
      }),
    );
  },
});

// Paginated history
export const listHistory = query({
  args: {
    userId: v.optional(v.id('users')),
    paginationOpts: paginationOptsValidator,
  },
  handler: async (
    ctx,
    { userId, paginationOpts },
  ): Promise<Paginated<StatusRow>> => {
    const result = userId
      ? await ctx.db
          .query('statuses')
          .withIndex('by_user_updatedAt', (q) => q.eq('userId', userId))
          .order('desc')
          .paginate(paginationOpts)
      : await ctx.db.query('statuses').order('desc').paginate(paginationOpts);
    const displayCache = new Map<string, StatusRow['user']>();
    const getDisplay = async (uid: Id<'users'>): Promise<StatusRow['user']> => {
      const key = uid as unknown as string;
      const cached = displayCache.get(key);
      if (cached) return cached;
      const user = await ctx.db.get(uid);
      if (!user) throw new ConvexError('User not found.');
      const imgId = getImageId(user);
      const imgUrl = imgId ? await ctx.storage.getUrl(imgId) : null;
      const display: StatusRow['user'] = {
        id: user._id,
        email: getEmail(user),
        name: getName(user),
        imageUrl: imgUrl,
      };
      displayCache.set(key, display);
      return display;
    };
    const statuses: StatusRow[] = [];
    for (const s of result.page) {
      const owner = await getDisplay(s.userId);
      const updatedBy =
        s.updatedBy && s.updatedBy !== s.userId
          ? await getDisplay(s.updatedBy)
          : null;
      statuses.push({
        user: owner,
        status: {
          id: s._id,
          message: s.message,
          updatedAt: s.updatedAt,
          updatedBy,
        },
      });
    }
    return {
      page: statuses,
      isDone: result.isDone,
      continueCursor: result.continueCursor,
    };
  },
});

export const endOfShiftUpdate = action({
  handler: async (ctx) => {
    const now = new Date(
      new Date().toLocaleString('en-US', {
        timeZone: 'America/Chicago',
      }),
    );
    const day = now.getDay(),
      hour = now.getHours(),
      minute = now.getMinutes();
    if (day == 0 || day === 6) return;
    const message = day === 5 ? 'Enjoying the weekend' : 'End of shift';
    if (hour === 17) {
      await ctx.runMutation(api.statuses.updateAllStatuses, { message });
    } else if (hour === 16) {
      const ms = ((60 - minute) % 60) * 60 * 1000;
      await ctx.scheduler.runAfter(ms, api.statuses.endOfShiftUpdate);
    } else return;
  },
});

export const automaticLunch = action({
  handler: async (ctx) => {
    const now = new Date(
      new Date().toLocaleString('en-US', {
        timeZone: 'America/Chicago',
      }),
    );
    const users = await ctx.runQuery(api.auth.getAllUsers);
    await Promise.all(
      users.map(async (user) => {
        if (user.automaticLunch && user.lunchTime) {
          const [hours, minutes] = user.lunchTime.split(':').map(Number);
          const userLunchTime = new Date(now);
          userLunchTime.setHours(hours, minutes, 0, 0);
          const diffInMs = userLunchTime.getTime() - now.getTime();
          // Only schedule if lunch is in the future today
          if (diffInMs > 0) {
            await ctx.scheduler.runAfter(
              diffInMs,
              api.statuses.createLunchStatus,
              { userId: user.id },
            );
          } else {
            console.warn(
              `Skipped ${user.name} - lunch time ${user.lunchTime} already passed.`
            );
          }
        }
      })
    );
  },
});
