import { ConvexError, v } from 'convex/values';

import type { Doc, Id } from './_generated/dataModel';
import { internalMutation, internalQuery, query } from './_generated/server';
import { getOwnedSpoon, getRequiredUserId } from './model';

const syncStatus = v.union(
  v.literal('up_to_date'),
  v.literal('behind'),
  v.literal('ahead'),
  v.literal('diverged'),
  v.literal('unknown'),
);

export const deriveSyncStatus = ({
  upstreamAheadBy,
  forkAheadBy,
}: {
  upstreamAheadBy: number;
  forkAheadBy: number;
}): Doc<'spoonRepositoryStates'>['status'] => {
  if (upstreamAheadBy === 0 && forkAheadBy === 0) return 'up_to_date';
  if (upstreamAheadBy > 0 && forkAheadBy === 0) return 'behind';
  if (upstreamAheadBy === 0 && forkAheadBy > 0) return 'ahead';
  if (upstreamAheadBy > 0 && forkAheadBy > 0) return 'diverged';
  return 'unknown';
};

export const getForSpoon = query({
  args: { spoonId: v.id('spoons') },
  handler: async (ctx, { spoonId }) => {
    const ownerId = await getRequiredUserId(ctx);
    await getOwnedSpoon(ctx, spoonId, ownerId);
    return await ctx.db
      .query('spoonRepositoryStates')
      .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
      .first();
  },
});

export const listForOwner = query({
  args: {},
  handler: async (ctx) => {
    const ownerId = await getRequiredUserId(ctx);
    return await ctx.db
      .query('spoonRepositoryStates')
      .withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
      .collect();
  },
});

export const getInternal = internalQuery({
  args: { spoonId: v.id('spoons'), ownerId: v.id('users') },
  handler: async (ctx, { spoonId, ownerId }) => {
    const state = await ctx.db
      .query('spoonRepositoryStates')
      .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
      .first();
    if (state && state.ownerId !== ownerId) {
      throw new ConvexError('Repository state ownership mismatch.');
    }
    return state;
  },
});

export const upsert = internalMutation({
  args: {
    spoonId: v.id('spoons'),
    ownerId: v.id('users'),
    upstreamFullName: v.string(),
    forkFullName: v.string(),
    upstreamDefaultBranch: v.string(),
    forkDefaultBranch: v.string(),
    upstreamHeadSha: v.optional(v.string()),
    forkHeadSha: v.optional(v.string()),
    mergeBaseSha: v.optional(v.string()),
    upstreamAheadBy: v.number(),
    forkAheadBy: v.number(),
    status: syncStatus,
    openForkPullRequestCount: v.number(),
    openUpstreamPullRequestCount: v.number(),
    lastCommitAt: v.optional(v.number()),
    rawCompareUrl: v.optional(v.string()),
  },
  handler: async (ctx, args): Promise<Id<'spoonRepositoryStates'>> => {
    const now = Date.now();
    const existing = await ctx.db
      .query('spoonRepositoryStates')
      .withIndex('by_spoon', (q) => q.eq('spoonId', args.spoonId))
      .first();
    const patch = {
      ownerId: args.ownerId,
      upstreamFullName: args.upstreamFullName,
      forkFullName: args.forkFullName,
      upstreamDefaultBranch: args.upstreamDefaultBranch,
      forkDefaultBranch: args.forkDefaultBranch,
      upstreamHeadSha: args.upstreamHeadSha,
      forkHeadSha: args.forkHeadSha,
      mergeBaseSha: args.mergeBaseSha,
      upstreamAheadBy: args.upstreamAheadBy,
      forkAheadBy: args.forkAheadBy,
      status: args.status,
      openForkPullRequestCount: args.openForkPullRequestCount,
      openUpstreamPullRequestCount: args.openUpstreamPullRequestCount,
      lastCommitAt: args.lastCommitAt,
      rawCompareUrl: args.rawCompareUrl,
      refreshedAt: now,
      updatedAt: now,
    };
    if (existing) {
      if (existing.ownerId !== args.ownerId) {
        throw new ConvexError('Repository state ownership mismatch.');
      }
      await ctx.db.patch(existing._id, patch);
      return existing._id;
    }
    return await ctx.db.insert('spoonRepositoryStates', {
      spoonId: args.spoonId,
      ...patch,
      createdAt: now,
    });
  },
});
