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

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

const provider = v.union(
  v.literal('github'),
  v.literal('gitea'),
  v.literal('gitlab'),
  v.literal('other'),
);

const visibility = v.union(
  v.literal('public'),
  v.literal('private'),
  v.literal('internal'),
  v.literal('unknown'),
);

const maintenanceMode = v.union(
  v.literal('watch'),
  v.literal('auto_pr'),
  v.literal('paused'),
);

const syncCadence = v.union(
  v.literal('daily'),
  v.literal('weekly'),
  v.literal('manual'),
);

const productionRefStrategy = v.union(
  v.literal('default_branch'),
  v.literal('latest_release'),
  v.literal('tag_pattern'),
);

const spoonStatus = v.union(
  v.literal('draft'),
  v.literal('active'),
  v.literal('needs_connection'),
  v.literal('paused'),
  v.literal('archived'),
);

const spoonSyncStatus = v.union(
  v.literal('unknown'),
  v.literal('up_to_date'),
  v.literal('behind'),
  v.literal('ahead'),
  v.literal('diverged'),
  v.literal('checking'),
  v.literal('conflict'),
  v.literal('error'),
);

const hasForkMetadata = (args: {
  forkOwner?: string;
  forkRepo?: string;
  forkUrl?: string;
}) =>
  Boolean(
    args.forkOwner?.trim() && args.forkRepo?.trim() && args.forkUrl?.trim(),
  );

export const listMine = query({
  args: {},
  handler: async (ctx) => {
    const ownerId = await getRequiredUserId(ctx);
    const spoons = await ctx.db
      .query('spoons')
      .withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
      .order('desc')
      .collect();
    return spoons.filter((spoon) => spoon.status !== 'archived');
  },
});

export const listMineWithState = query({
  args: {},
  handler: async (ctx) => {
    const ownerId = await getRequiredUserId(ctx);
    const spoons = (
      await ctx.db
        .query('spoons')
        .withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
        .order('desc')
        .collect()
    ).filter((spoon) => spoon.status !== 'archived');

    return await Promise.all(
      spoons.map(async (spoon) => {
        const [state, ignoredChanges, threads] = await Promise.all([
          ctx.db
            .query('spoonRepositoryStates')
            .withIndex('by_spoon', (q) => q.eq('spoonId', spoon._id))
            .first(),
          ctx.db
            .query('ignoredUpstreamChanges')
            .withIndex('by_spoon', (q) => q.eq('spoonId', spoon._id))
            .collect(),
          ctx.db
            .query('threads')
            .withIndex('by_spoon', (q) => q.eq('spoonId', spoon._id))
            .order('desc')
            .collect(),
        ]);
        const ignoredShas = new Set(
          ignoredChanges.flatMap((change) => change.commitShas),
        );
        const rawUpstreamAheadBy =
          state?.upstreamAheadBy ?? spoon.upstreamAheadBy ?? 0;
        const effectiveUpstreamAheadBy = Math.max(
          0,
          rawUpstreamAheadBy - ignoredShas.size,
        );
        const openThreads = threads.filter(
          (thread) =>
            !['resolved', 'ignored', 'failed', 'cancelled'].includes(
              thread.status,
            ),
        );
        return {
          ...spoon,
          rawUpstreamAheadBy,
          effectiveUpstreamAheadBy,
          ignoredUpstreamCount: ignoredShas.size,
          forkAheadBy: state?.forkAheadBy ?? spoon.forkAheadBy ?? 0,
          openThreadCount: openThreads.length,
          latestThreadStatus: threads[0]?.status,
        };
      }),
    );
  },
});

export const get = query({
  args: { spoonId: v.id('spoons') },
  handler: async (ctx, { spoonId }) => {
    const ownerId = await getRequiredUserId(ctx);
    return getOwnedSpoon(ctx, spoonId, ownerId);
  },
});

export const getDetails = query({
  args: { spoonId: v.id('spoons') },
  handler: async (ctx, { spoonId }) => {
    const ownerId = await getRequiredUserId(ctx);
    const spoon = await getOwnedSpoon(ctx, spoonId, ownerId);
    const [
      state,
      settings,
      latestReview,
      recentRuns,
      agentRequests,
      ignoredChanges,
    ] = await Promise.all([
      ctx.db
        .query('spoonRepositoryStates')
        .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
        .first(),
      ctx.db
        .query('spoonSettings')
        .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
        .first(),
      ctx.db
        .query('aiReviews')
        .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
        .order('desc')
        .first(),
      ctx.db
        .query('syncRuns')
        .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
        .order('desc')
        .take(10),
      ctx.db
        .query('agentRequests')
        .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
        .order('desc')
        .take(10),
      ctx.db
        .query('ignoredUpstreamChanges')
        .withIndex('by_spoon', (q) => q.eq('spoonId', spoonId))
        .collect(),
    ]);
    const ignoredShas = new Set(
      ignoredChanges.flatMap((change) => change.commitShas),
    );
    const effectiveUpstreamAheadBy = Math.max(
      0,
      (state?.upstreamAheadBy ?? spoon.upstreamAheadBy ?? 0) - ignoredShas.size,
    );
    return {
      spoon,
      state,
      settings,
      latestReview,
      recentRuns,
      agentRequests,
      ignoredChanges,
      effectiveUpstreamAheadBy,
    };
  },
});

export const getOwnedForAction = internalQuery({
  args: { spoonId: v.id('spoons'), ownerId: v.id('users') },
  handler: async (ctx, { spoonId, ownerId }) => {
    return await getOwnedSpoon(ctx, spoonId, ownerId);
  },
});

export const createManual = mutation({
  args: {
    name: v.string(),
    description: v.optional(v.string()),
    provider,
    upstreamOwner: v.string(),
    upstreamRepo: v.string(),
    upstreamDefaultBranch: v.string(),
    upstreamUrl: v.string(),
    forkOwner: v.optional(v.string()),
    forkRepo: v.optional(v.string()),
    forkDefaultBranch: v.optional(v.string()),
    forkUrl: v.optional(v.string()),
    visibility,
    maintenanceMode,
    syncCadence,
    productionRefStrategy,
    tagPattern: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const ownerId = await getRequiredUserId(ctx);
    const now = Date.now();
    const forkOwner = optionalText(args.forkOwner);
    const forkRepo = optionalText(args.forkRepo);
    const forkDefaultBranch = optionalText(args.forkDefaultBranch);
    const forkUrl = optionalText(args.forkUrl);
    const status = hasForkMetadata({ forkOwner, forkRepo, forkUrl })
      ? 'draft'
      : 'needs_connection';

    const spoonId = await ctx.db.insert('spoons', {
      ownerId,
      name: requireText(args.name, 'Spoon name'),
      description: optionalText(args.description),
      provider: args.provider,
      upstreamOwner: requireText(args.upstreamOwner, 'Upstream owner'),
      upstreamRepo: requireText(args.upstreamRepo, 'Upstream repository'),
      upstreamDefaultBranch: requireText(
        args.upstreamDefaultBranch,
        'Upstream default branch',
      ),
      upstreamUrl: requireText(args.upstreamUrl, 'Upstream URL'),
      forkOwner,
      forkRepo,
      forkDefaultBranch,
      forkUrl,
      visibility: args.visibility,
      maintenanceMode: args.maintenanceMode,
      syncCadence: args.syncCadence,
      productionRefStrategy: args.productionRefStrategy,
      tagPattern: optionalText(args.tagPattern),
      status,
      syncStatus: 'unknown',
      createdAt: now,
      updatedAt: now,
    });
    await ctx.db.insert('spoonSettings', {
      spoonId,
      ownerId,
      autoRefreshEnabled: true,
      autoReviewEnabled: true,
      autoSyncEnabled: false,
      requireAiLowRiskForSync: true,
      requireCleanCompareForSync: true,
      ignoredFilePatterns: [],
      importantFilePatterns: [],
      createdAt: now,
      updatedAt: now,
    });
    await ctx.db.insert('spoonAgentSettings', {
      spoonId,
      ownerId,
      enabled: true,
      runtime: 'opencode',
      defaultBaseBranch: forkDefaultBranch ?? args.upstreamDefaultBranch,
      branchPrefix: 'spoon/agent',
      agentModel: '',
      reasoningEffort: 'medium',
      maxJobDurationMs: 1_800_000,
      maxOutputBytes: 200_000,
      envFilePath: '.env.local',
      materializeEnvFileByDefault: false,
      autoDetectCommands: true,
      allowUserFileEditing: true,
      aiProviderProfileId: undefined,
      createdAt: now,
      updatedAt: now,
    });
    return spoonId;
  },
});

export const patchSyncFields = internalMutation({
  args: {
    spoonId: v.id('spoons'),
    syncStatus: v.optional(spoonSyncStatus),
    upstreamAheadBy: v.optional(v.number()),
    forkAheadBy: v.optional(v.number()),
    lastMergeBaseCommit: v.optional(v.string()),
    lastUpstreamCommit: v.optional(v.string()),
    lastForkCommit: v.optional(v.string()),
    lastSyncRunId: v.optional(v.id('syncRuns')),
    lastAiReviewId: v.optional(v.id('aiReviews')),
    lastGithubRefreshAt: v.optional(v.number()),
    lastSuccessfulRefreshAt: v.optional(v.number()),
    lastCheckedAt: v.optional(v.number()),
    lastError: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const patch: Partial<Doc<'spoons'>> = { updatedAt: Date.now() };
    if (args.syncStatus !== undefined) patch.syncStatus = args.syncStatus;
    if (args.upstreamAheadBy !== undefined) {
      patch.upstreamAheadBy = args.upstreamAheadBy;
    }
    if (args.forkAheadBy !== undefined) patch.forkAheadBy = args.forkAheadBy;
    if (args.lastMergeBaseCommit !== undefined) {
      patch.lastMergeBaseCommit = args.lastMergeBaseCommit;
    }
    if (args.lastUpstreamCommit !== undefined) {
      patch.lastUpstreamCommit = args.lastUpstreamCommit;
    }
    if (args.lastForkCommit !== undefined) {
      patch.lastForkCommit = args.lastForkCommit;
    }
    if (args.lastSyncRunId !== undefined)
      patch.lastSyncRunId = args.lastSyncRunId;
    if (args.lastAiReviewId !== undefined)
      patch.lastAiReviewId = args.lastAiReviewId;
    if (args.lastGithubRefreshAt !== undefined) {
      patch.lastGithubRefreshAt = args.lastGithubRefreshAt;
    }
    if (args.lastSuccessfulRefreshAt !== undefined) {
      patch.lastSuccessfulRefreshAt = args.lastSuccessfulRefreshAt;
    }
    if (args.lastCheckedAt !== undefined)
      patch.lastCheckedAt = args.lastCheckedAt;
    if (args.lastError !== undefined) patch.lastError = args.lastError;
    await ctx.db.patch(args.spoonId, patch);
    return { success: true };
  },
});

export const updateSettings = mutation({
  args: {
    spoonId: v.id('spoons'),
    maintenanceMode: v.optional(maintenanceMode),
    syncCadence: v.optional(syncCadence),
    productionRefStrategy: v.optional(productionRefStrategy),
    tagPattern: v.optional(v.string()),
    status: v.optional(spoonStatus),
  },
  handler: async (ctx, args) => {
    const ownerId = await getRequiredUserId(ctx);
    await getOwnedSpoon(ctx, args.spoonId, ownerId);
    const patch: Partial<Doc<'spoons'>> = { updatedAt: Date.now() };
    if (args.maintenanceMode) patch.maintenanceMode = args.maintenanceMode;
    if (args.syncCadence) patch.syncCadence = args.syncCadence;
    if (args.productionRefStrategy) {
      patch.productionRefStrategy = args.productionRefStrategy;
    }
    if (args.tagPattern !== undefined)
      patch.tagPattern = optionalText(args.tagPattern);
    if (args.status) patch.status = args.status;
    await ctx.db.patch(args.spoonId, patch);
    return { success: true };
  },
});

export const archive = mutation({
  args: { spoonId: v.id('spoons') },
  handler: async (ctx, { spoonId }) => {
    const ownerId = await getRequiredUserId(ctx);
    const spoon = await getOwnedSpoon(ctx, spoonId, ownerId);
    if (spoon.status === 'archived')
      throw new ConvexError('Spoon is archived.');
    await ctx.db.patch(spoonId, {
      status: 'archived',
      updatedAt: Date.now(),
    });
    return { success: true };
  },
});
