import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

const pollOptionValidator = v.object({
  id: v.string(),
  text: v.string(),
});

const pollDocValidator = v.object({
  _id: v.id("polls"),
  _creationTime: v.number(),
  channelId: v.id("channels"),
  createdBy: v.id("userProfiles"),
  question: v.string(),
  options: v.array(pollOptionValidator),
  allowMultiple: v.boolean(),
  disclosed: v.boolean(),
  closed: v.boolean(),
  closesAt: v.optional(v.number()),
  createdAt: v.number(),
});

const pollReactionValidator = v.object({
  emoji: v.string(),
  count: v.number(),
  me: v.boolean(),
});

const pollResultsValidator = v.object({
  poll: pollDocValidator,
  totals: v.record(v.string(), v.number()),
  totalVotes: v.number(),
  myVote: v.union(v.array(v.string()), v.null()),
  // Aggregated as an array — arrays keep emoji out of object field
  // names (Convex's return-value validator rejects non-ASCII fields,
  // same issue we hit on messages.list).
  reactions: v.array(pollReactionValidator),
});

export const create = mutation({
  args: {
    channelId: v.id("channels"),
    createdBy: v.id("userProfiles"),
    question: v.string(),
    options: v.array(pollOptionValidator),
    allowMultiple: v.boolean(),
    disclosed: v.boolean(),
    closesAt: v.optional(v.number()),
  },
  returns: v.id("polls"),
  handler: async (ctx, args) => {
    const question = args.question.trim();
    if (question.length === 0) {
      throw new Error("Poll question cannot be empty");
    }
    if (question.length > 500) {
      throw new Error("Poll question is too long");
    }

    const cleanOptions = args.options
      .map((o) => ({ id: o.id, text: o.text.trim() }))
      .filter((o) => o.text.length > 0);

    if (cleanOptions.length < 2) {
      throw new Error("Polls need at least 2 options");
    }
    if (cleanOptions.length > 20) {
      throw new Error("Polls support at most 20 options");
    }

    // Enforce unique option ids so vote diffing is unambiguous.
    const seen = new Set<string>();
    for (const opt of cleanOptions) {
      if (seen.has(opt.id)) {
        throw new Error("Duplicate option id");
      }
      seen.add(opt.id);
    }

    const pollId = await ctx.db.insert("polls", {
      channelId: args.channelId,
      createdBy: args.createdBy,
      question,
      options: cleanOptions,
      allowMultiple: args.allowMultiple,
      disclosed: args.disclosed,
      closed: false,
      closesAt: args.closesAt,
      createdAt: Date.now(),
    });

    return pollId;
  },
});

export const vote = mutation({
  args: {
    pollId: v.id("polls"),
    userId: v.id("userProfiles"),
    optionIds: v.array(v.string()),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const poll = await ctx.db.get(args.pollId);
    if (!poll) throw new Error("Poll not found");
    if (poll.closed) throw new Error("Poll is closed");
    if (poll.closesAt && poll.closesAt < Date.now()) {
      throw new Error("Poll has expired");
    }

    // Validate the submitted option ids exist on the poll.
    const validIds = new Set(poll.options.map((o) => o.id));
    for (const id of args.optionIds) {
      if (!validIds.has(id)) {
        throw new Error(`Unknown option id "${id}"`);
      }
    }

    if (args.optionIds.length === 0) {
      throw new Error("Select at least one option");
    }
    if (!poll.allowMultiple && args.optionIds.length > 1) {
      throw new Error("This poll only allows one answer");
    }

    // Upsert: one row per (pollId, userId).
    const existing = await ctx.db
      .query("pollVotes")
      .withIndex("by_poll_and_user", (q) =>
        q.eq("pollId", args.pollId).eq("userId", args.userId),
      )
      .unique();

    if (existing) {
      await ctx.db.patch(existing._id, {
        optionIds: args.optionIds,
        votedAt: Date.now(),
      });
    } else {
      await ctx.db.insert("pollVotes", {
        pollId: args.pollId,
        userId: args.userId,
        optionIds: args.optionIds,
        votedAt: Date.now(),
      });
    }
    return null;
  },
});

export const clearVote = mutation({
  args: {
    pollId: v.id("polls"),
    userId: v.id("userProfiles"),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("pollVotes")
      .withIndex("by_poll_and_user", (q) =>
        q.eq("pollId", args.pollId).eq("userId", args.userId),
      )
      .unique();
    if (existing) {
      await ctx.db.delete(existing._id);
    }
    return null;
  },
});

export const close = mutation({
  args: {
    pollId: v.id("polls"),
    userId: v.id("userProfiles"),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const poll = await ctx.db.get(args.pollId);
    if (!poll) throw new Error("Poll not found");
    if (poll.createdBy !== args.userId) {
      throw new Error("Only the poll creator can close it");
    }
    if (poll.closed) return null;
    await ctx.db.patch(args.pollId, { closed: true });
    return null;
  },
});

export const remove = mutation({
  args: {
    pollId: v.id("polls"),
    userId: v.id("userProfiles"),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const poll = await ctx.db.get(args.pollId);
    if (!poll) return null;
    if (poll.createdBy !== args.userId) {
      throw new Error("Only the poll creator can delete it");
    }
    const votes = await ctx.db
      .query("pollVotes")
      .withIndex("by_poll", (q) => q.eq("pollId", args.pollId))
      .collect();
    await Promise.all(votes.map((v) => ctx.db.delete(v._id)));
    const reactions = await ctx.db
      .query("pollReactions")
      .withIndex("by_poll", (q) => q.eq("pollId", args.pollId))
      .collect();
    await Promise.all(reactions.map((r) => ctx.db.delete(r._id)));
    await ctx.db.delete(args.pollId);
    return null;
  },
});

export const listByChannel = query({
  args: {
    channelId: v.id("channels"),
  },
  returns: v.array(pollDocValidator),
  handler: async (ctx, args) => {
    const polls = await ctx.db
      .query("polls")
      .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
      .order("desc")
      .collect();
    return polls;
  },
});

export const get = query({
  args: {
    pollId: v.id("polls"),
    userId: v.optional(v.id("userProfiles")),
  },
  returns: v.union(pollResultsValidator, v.null()),
  handler: async (ctx, args) => {
    const poll = await ctx.db.get(args.pollId);
    if (!poll) return null;

    const votes = await ctx.db
      .query("pollVotes")
      .withIndex("by_poll", (q) => q.eq("pollId", args.pollId))
      .collect();

    // Tally totals per option. Voters that picked multiple options
    // each contribute a +1 to every option they picked.
    const totals: Record<string, number> = {};
    for (const opt of poll.options) {
      totals[opt.id] = 0;
    }
    for (const vote of votes) {
      for (const id of vote.optionIds) {
        if (totals[id] !== undefined) {
          totals[id] += 1;
        }
      }
    }

    let myVote: string[] | null = null;
    if (args.userId) {
      const mine = votes.find((v) => v.userId === args.userId);
      myVote = mine ? mine.optionIds : null;
    }

    // Aggregate reactions into an array of {emoji, count, me} rows.
    // Using a Map avoids putting unicode surrogates into object field
    // names, which Convex's return-value validator would reject.
    const reactionDocs = await ctx.db
      .query("pollReactions")
      .withIndex("by_poll", (q) => q.eq("pollId", args.pollId))
      .collect();
    const reactionMap = new Map<string, { count: number; me: boolean }>();
    for (const r of reactionDocs) {
      let entry = reactionMap.get(r.emoji);
      if (!entry) {
        entry = { count: 0, me: false };
        reactionMap.set(r.emoji, entry);
      }
      entry.count++;
      if (args.userId && r.userId === args.userId) {
        entry.me = true;
      }
    }
    const reactions: Array<{ emoji: string; count: number; me: boolean }> = [];
    reactionMap.forEach((info, emoji) => {
      reactions.push({ emoji, count: info.count, me: info.me });
    });

    return {
      poll,
      totals,
      totalVotes: votes.length,
      myVote,
      reactions,
    };
  },
});

/**
 * Toggle-add a reaction on a poll. Idempotent per (pollId, userId,
 * emoji) — re-adding the same reaction is a no-op.
 */
export const addReaction = mutation({
  args: {
    pollId: v.id("polls"),
    userId: v.id("userProfiles"),
    emoji: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("pollReactions")
      .withIndex("by_poll_user_emoji", (q) =>
        q
          .eq("pollId", args.pollId)
          .eq("userId", args.userId)
          .eq("emoji", args.emoji),
      )
      .unique();
    if (!existing) {
      await ctx.db.insert("pollReactions", {
        pollId: args.pollId,
        userId: args.userId,
        emoji: args.emoji,
      });
    }
    return null;
  },
});

/**
 * Remove a reaction on a poll. No-op if the user hasn't reacted
 * with that emoji.
 */
export const removeReaction = mutation({
  args: {
    pollId: v.id("polls"),
    userId: v.id("userProfiles"),
    emoji: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("pollReactions")
      .withIndex("by_poll_user_emoji", (q) =>
        q
          .eq("pollId", args.pollId)
          .eq("userId", args.userId)
          .eq("emoji", args.emoji),
      )
      .unique();
    if (existing) {
      await ctx.db.delete(existing._id);
    }
    return null;
  },
});
