import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@prisma/client'
import { router, adminProcedure, protectedProcedure } from '../trpc'
import { logAudit } from '@/server/utils/audit'
import { generateInviteToken, getInviteExpiryMs } from '@/server/utils/invite'
import { sendJuryInvitationEmail } from '@/lib/email'

const capModeEnum = z.enum(['HARD', 'SOFT', 'NONE'])

export const juryGroupRouter = router({
  /**
   * Create a new jury group for a competition
   */
  create: adminProcedure
    .input(
      z.object({
        competitionId: z.string(),
        name: z.string().min(1).max(255),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
        description: z.string().optional(),
        sortOrder: z.number().int().nonnegative().default(0),
        defaultMaxAssignments: z.number().int().positive().default(20),
        defaultCapMode: capModeEnum.default('SOFT'),
        softCapBuffer: z.number().int().nonnegative().default(2),
        categoryQuotasEnabled: z.boolean().default(false),
        defaultCategoryQuotas: z
          .record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
          .optional(),
        allowJurorCapAdjustment: z.boolean().default(false),
        allowJurorRatioAdjustment: z.boolean().default(false),
      })
    )
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.competition.findUniqueOrThrow({
        where: { id: input.competitionId },
      })

      const { defaultCategoryQuotas, ...rest } = input

      const juryGroup = await ctx.prisma.juryGroup.create({
        data: {
          ...rest,
          defaultCategoryQuotas: defaultCategoryQuotas ?? undefined,
        },
      })

      // Audit outside transaction so failures don't roll back the create
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'JuryGroup',
        entityId: juryGroup.id,
        detailsJson: { name: input.name, competitionId: input.competitionId },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return juryGroup
    }),

  /**
   * Get jury group by ID with members
   */
  getById: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const group = await ctx.prisma.juryGroup.findUnique({
        where: { id: input.id },
        include: {
          members: {
            include: {
              user: {
                select: { id: true, name: true, email: true, role: true },
              },
            },
            orderBy: { joinedAt: 'asc' },
          },
        },
      })

      if (!group) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Jury group not found' })
      }

      return group
    }),

  /**
   * List jury groups for a competition
   */
  list: protectedProcedure
    .input(z.object({ competitionId: z.string() }))
    .query(async ({ ctx, input }) => {
      return ctx.prisma.juryGroup.findMany({
        where: { competitionId: input.competitionId },
        orderBy: { sortOrder: 'asc' },
        include: {
          _count: { select: { members: true, assignments: true } },
          rounds: {
            select: { id: true, name: true, roundType: true, status: true },
            orderBy: { sortOrder: 'asc' },
          },
          awards: {
            select: { id: true, name: true, status: true },
            orderBy: { sortOrder: 'asc' },
          },
          members: {
            take: 5,
            orderBy: { joinedAt: 'asc' },
            select: {
              id: true,
              role: true,
              user: { select: { id: true, name: true, email: true } },
            },
          },
        },
      })
    }),

  /**
   * Update jury group settings
   */
  update: adminProcedure
    .input(
      z.object({
        id: z.string(),
        name: z.string().min(1).max(255).optional(),
        slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional(),
        description: z.string().optional(),
        sortOrder: z.number().int().nonnegative().optional(),
        defaultMaxAssignments: z.number().int().positive().optional(),
        defaultCapMode: capModeEnum.optional(),
        softCapBuffer: z.number().int().nonnegative().optional(),
        categoryQuotasEnabled: z.boolean().optional(),
        defaultCategoryQuotas: z
          .record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
          .nullable()
          .optional(),
        allowJurorCapAdjustment: z.boolean().optional(),
        allowJurorRatioAdjustment: z.boolean().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, defaultCategoryQuotas, ...rest } = input

      return ctx.prisma.juryGroup.update({
        where: { id },
        data: {
          ...rest,
          ...(defaultCategoryQuotas !== undefined
            ? {
                defaultCategoryQuotas:
                  defaultCategoryQuotas === null
                    ? Prisma.JsonNull
                    : (defaultCategoryQuotas as Prisma.InputJsonValue),
              }
            : {}),
        },
      })
    }),

  /**
   * Add a member to a jury group
   */
  addMember: adminProcedure
    .input(
      z.object({
        juryGroupId: z.string(),
        userId: z.string(),
        role: z.enum(['CHAIR', 'MEMBER', 'OBSERVER']).default('MEMBER'),
        maxAssignmentsOverride: z.number().int().positive().nullable().optional(),
        capModeOverride: capModeEnum.nullable().optional(),
        categoryQuotasOverride: z
          .record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
          .nullable()
          .optional(),
        preferredStartupRatio: z.number().min(0).max(1).nullable().optional(),
        availabilityNotes: z.string().nullable().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Verify the user exists
      await ctx.prisma.user.findUniqueOrThrow({
        where: { id: input.userId },
      })

      // Check if already a member
      const existing = await ctx.prisma.juryGroupMember.findUnique({
        where: {
          juryGroupId_userId: {
            juryGroupId: input.juryGroupId,
            userId: input.userId,
          },
        },
      })

      if (existing) {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'User is already a member of this jury group',
        })
      }

      const member = await ctx.prisma.juryGroupMember.create({
        data: {
          juryGroupId: input.juryGroupId,
          userId: input.userId,
          role: input.role,
          maxAssignmentsOverride: input.maxAssignmentsOverride ?? undefined,
          capModeOverride: input.capModeOverride ?? undefined,
          categoryQuotasOverride: input.categoryQuotasOverride ?? undefined,
          preferredStartupRatio: input.preferredStartupRatio ?? undefined,
          availabilityNotes: input.availabilityNotes ?? undefined,
        },
        include: {
          user: { select: { id: true, name: true, email: true, role: true } },
        },
      })

      // Audit outside transaction so failures don't roll back the member add
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'JuryGroupMember',
        entityId: member.id,
        detailsJson: {
          juryGroupId: input.juryGroupId,
          addedUserId: input.userId,
          role: input.role,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return member
    }),

  /**
   * Remove a member from a jury group
   */
  removeMember: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const existing = await ctx.prisma.juryGroupMember.findUniqueOrThrow({
        where: { id: input.id },
      })

      await ctx.prisma.juryGroupMember.delete({ where: { id: input.id } })

      // Audit outside transaction so failures don't roll back the member removal
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'JuryGroupMember',
        entityId: input.id,
        detailsJson: {
          juryGroupId: existing.juryGroupId,
          removedUserId: existing.userId,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return existing
    }),

  /**
   * Delete a jury group entirely
   */
  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
        where: { id: input.id },
        include: {
          _count: { select: { members: true, assignments: true, rounds: true } },
        },
      })

      // Unlink any rounds that reference this jury group
      await ctx.prisma.round.updateMany({
        where: { juryGroupId: input.id },
        data: { juryGroupId: null },
      })

      // Delete all members first (cascade should handle this, but be explicit)
      await ctx.prisma.juryGroupMember.deleteMany({
        where: { juryGroupId: input.id },
      })

      await ctx.prisma.juryGroup.delete({ where: { id: input.id } })

      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'JuryGroup',
        entityId: input.id,
        detailsJson: {
          name: group.name,
          competitionId: group.competitionId,
          memberCount: group._count.members,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { success: true, name: group.name }
    }),

  /**
   * Update a jury group member's role/overrides
   */
  updateMember: adminProcedure
    .input(
      z.object({
        id: z.string(),
        role: z.enum(['CHAIR', 'MEMBER', 'OBSERVER']).optional(),
        maxAssignmentsOverride: z.number().int().positive().nullable().optional(),
        capModeOverride: capModeEnum.nullable().optional(),
        categoryQuotasOverride: z
          .record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
          .nullable()
          .optional(),
        preferredStartupRatio: z.number().min(0).max(1).nullable().optional(),
        availabilityNotes: z.string().nullable().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, categoryQuotasOverride, ...rest } = input

      return ctx.prisma.juryGroupMember.update({
        where: { id },
        data: {
          ...rest,
          ...(categoryQuotasOverride !== undefined
            ? {
                categoryQuotasOverride:
                  categoryQuotasOverride === null
                    ? Prisma.JsonNull
                    : (categoryQuotasOverride as Prisma.InputJsonValue),
              }
            : {}),
        },
        include: {
          user: { select: { id: true, name: true, email: true, role: true } },
        },
      })
    }),

  /**
   * Review self-service values set by jurors during onboarding.
   * Returns members who have self-service cap or ratio adjustments.
   */
  reviewSelfServiceValues: adminProcedure
    .input(z.object({ juryGroupId: z.string() }))
    .query(async ({ ctx, input }) => {
      const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
        where: { id: input.juryGroupId },
        select: {
          id: true,
          name: true,
          defaultMaxAssignments: true,
          allowJurorCapAdjustment: true,
          allowJurorRatioAdjustment: true,
        },
      })

      const members = await ctx.prisma.juryGroupMember.findMany({
        where: {
          juryGroupId: input.juryGroupId,
          OR: [
            { selfServiceCap: { not: null } },
            { selfServiceRatio: { not: null } },
          ],
        },
        include: {
          user: { select: { id: true, name: true, email: true } },
        },
        orderBy: { joinedAt: 'asc' },
      })

      return {
        group,
        members: members.map((m) => ({
          id: m.id,
          userId: m.userId,
          userName: m.user.name,
          userEmail: m.user.email,
          role: m.role,
          adminCap: m.maxAssignmentsOverride ?? group.defaultMaxAssignments,
          selfServiceCap: m.selfServiceCap,
          selfServiceRatio: m.selfServiceRatio,
          preferredStartupRatio: m.preferredStartupRatio,
        })),
      }
    }),

  /**
   * Bulk invite new users as jury group members — creates accounts, assigns JURY_MEMBER role, sends invite emails
   */
  bulkInviteMembers: adminProcedure
    .input(
      z.object({
        juryGroupId: z.string(),
        role: z.enum(['CHAIR', 'MEMBER', 'OBSERVER']).default('MEMBER'),
        invitees: z.array(
          z.object({
            name: z.string().optional(),
            email: z.string().email(),
          })
        ).min(1).max(50),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
        where: { id: input.juryGroupId },
        select: { id: true, name: true },
      })

      const results: Array<{ email: string; status: 'created' | 'existing' | 'error'; error?: string }> = []

      for (const invitee of input.invitees) {
        try {
          let user = await ctx.prisma.user.findUnique({
            where: { email: invitee.email },
            select: { id: true, status: true, role: true },
          })

          if (!user) {
            const inviteToken = generateInviteToken()
            const expiryMs = await getInviteExpiryMs(ctx.prisma)

            user = await ctx.prisma.user.create({
              data: {
                email: invitee.email,
                name: invitee.name || null,
                role: 'JURY_MEMBER',
                roles: ['JURY_MEMBER'],
                status: 'INVITED',
                inviteToken,
                inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
              },
              select: { id: true, status: true, role: true },
            })

            const inviteUrl = `${process.env.NEXTAUTH_URL}/accept-invite?token=${inviteToken}`
            try {
              await sendJuryInvitationEmail(
                invitee.email,
                invitee.name || null,
                inviteUrl,
                group.name
              )
            } catch {
              // Email failure shouldn't block the invite
            }

            results.push({ email: invitee.email, status: 'created' })
          } else {
            results.push({ email: invitee.email, status: 'existing' })
          }

          // Add as jury group member (skip if already added)
          const existing = await ctx.prisma.juryGroupMember.findUnique({
            where: {
              juryGroupId_userId: { juryGroupId: input.juryGroupId, userId: user.id },
            },
          })
          if (!existing) {
            await ctx.prisma.juryGroupMember.create({
              data: {
                juryGroupId: input.juryGroupId,
                userId: user.id,
                role: input.role,
              },
            })
          }
        } catch (err) {
          results.push({
            email: invitee.email,
            status: 'error',
            error: err instanceof Error ? err.message : 'Unknown error',
          })
        }
      }

      await logAudit({
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'JuryGroupMember',
        entityId: input.juryGroupId,
        detailsJson: {
          action: 'BULK_INVITE',
          groupName: group.name,
          count: input.invitees.length,
          results,
        },
      })

      return {
        created: results.filter((r) => r.status === 'created').length,
        existing: results.filter((r) => r.status === 'existing').length,
        errors: results.filter((r) => r.status === 'error').length,
        results,
      }
    }),
})
