import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { Prisma } from '@prisma/client'
import {
  router,
  protectedProcedure,
  adminProcedure,
} from '../trpc'
import { getPresignedUrl, ensureBucket } from '@/lib/minio'
import { logAudit } from '../utils/audit'

// Bucket for learning resources
export const LEARNING_BUCKET = 'mopc-learning'

// Access rule schema for fine-grained access control
const accessRuleSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('everyone') }),
  z.object({ type: z.literal('roles'), roles: z.array(z.string()) }),
  z.object({ type: z.literal('jury_group'), juryGroupIds: z.array(z.string()) }),
  z.object({ type: z.literal('round'), roundIds: z.array(z.string()) }),
])

type AccessRule = z.infer<typeof accessRuleSchema>

/**
 * Evaluate whether a user can access a resource based on its accessJson rules.
 * null/empty = everyone. Rules are OR-combined (match ANY rule = access).
 */
async function canUserAccessResource(
  prisma: { juryGroupMember: { findFirst: Function }; assignment: { findFirst: Function } },
  userId: string,
  userRole: string,
  accessJson: unknown,
): Promise<boolean> {
  // null or empty = everyone
  if (!accessJson) return true

  let rules: AccessRule[]
  try {
    const parsed = accessJson as unknown[]
    if (!Array.isArray(parsed) || parsed.length === 0) return true
    rules = parsed as AccessRule[]
  } catch (err) {
    console.error('Failed to parse learning resource access rules JSON:', err)
    return true
  }

  for (const rule of rules) {
    if (rule.type === 'everyone') return true

    if (rule.type === 'roles') {
      if (rule.roles.includes(userRole)) return true
    }

    if (rule.type === 'jury_group') {
      const membership = await prisma.juryGroupMember.findFirst({
        where: {
          userId,
          juryGroupId: { in: rule.juryGroupIds },
        },
      })
      if (membership) return true
    }

    if (rule.type === 'round') {
      const assignment = await prisma.assignment.findFirst({
        where: {
          userId,
          roundId: { in: rule.roundIds },
        },
      })
      if (assignment) return true
    }
  }

  return false
}

export const learningResourceRouter = router({
  /**
   * List all resources (admin view)
   */
  list: adminProcedure
    .input(
      z.object({
        programId: z.string().optional(),
        isPublished: z.boolean().optional(),
        page: z.number().int().min(1).default(1),
        perPage: z.number().int().min(1).max(100).default(50),
      })
    )
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = {}

      if (input.programId !== undefined) {
        where.programId = input.programId
      }
      if (input.isPublished !== undefined) {
        where.isPublished = input.isPublished
      }

      const [data, total] = await Promise.all([
        ctx.prisma.learningResource.findMany({
          where,
          include: {
            program: { select: { id: true, name: true, year: true } },
            createdBy: { select: { id: true, name: true, email: true } },
            _count: { select: { accessLogs: true } },
          },
          orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
          skip: (input.page - 1) * input.perPage,
          take: input.perPage,
        }),
        ctx.prisma.learningResource.count({ where }),
      ])

      return {
        data,
        total,
        page: input.page,
        perPage: input.perPage,
        totalPages: Math.ceil(total / input.perPage),
      }
    }),

  /**
   * Get resources accessible to the current user (jury/mentor/observer view)
   */
  myResources: protectedProcedure
    .input(
      z.object({
        programId: z.string().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const where: Record<string, unknown> = {
        isPublished: true,
      }

      if (input.programId) {
        where.OR = [{ programId: input.programId }, { programId: null }]
      }

      const resources = await ctx.prisma.learningResource.findMany({
        where,
        orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }],
      })

      // Filter by access rules in application code (small dataset)
      const accessible = []
      for (const resource of resources) {
        const allowed = await canUserAccessResource(
          ctx.prisma,
          ctx.user.id,
          ctx.user.role,
          resource.accessJson,
        )
        if (allowed) accessible.push(resource)
      }

      return { resources: accessible }
    }),

  /**
   * Get a single resource by ID
   */
  get: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const resource = await ctx.prisma.learningResource.findUniqueOrThrow({
        where: { id: input.id },
        include: {
          program: { select: { id: true, name: true, year: true } },
          createdBy: { select: { id: true, name: true, email: true } },
        },
      })

      // Check access for non-admins
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)
      if (!isAdmin) {
        if (!resource.isPublished) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'This resource is not available',
          })
        }

        const allowed = await canUserAccessResource(
          ctx.prisma,
          ctx.user.id,
          ctx.user.role,
          resource.accessJson,
        )
        if (!allowed) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this resource',
          })
        }
      }

      return resource
    }),

  /**
   * Get download URL for a resource file
   */
  getDownloadUrl: protectedProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const resource = await ctx.prisma.learningResource.findUniqueOrThrow({
        where: { id: input.id },
      })

      if (!resource.bucket || !resource.objectKey) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'This resource does not have a file',
        })
      }

      // Check access for non-admins
      const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role)
      if (!isAdmin) {
        if (!resource.isPublished) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'This resource is not available',
          })
        }

        const allowed = await canUserAccessResource(
          ctx.prisma,
          ctx.user.id,
          ctx.user.role,
          resource.accessJson,
        )
        if (!allowed) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'You do not have access to this resource',
          })
        }
      }

      // Log access
      await ctx.prisma.resourceAccess.create({
        data: {
          resourceId: resource.id,
          userId: ctx.user.id,
          ipAddress: ctx.ip,
        },
      })

      const url = await getPresignedUrl(resource.bucket, resource.objectKey, 'GET', 900)
      return { url }
    }),

  /**
   * Log access when user opens a resource detail page
   */
  logAccess: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.resourceAccess.create({
        data: {
          resourceId: input.id,
          userId: ctx.user.id,
          ipAddress: ctx.ip,
        },
      })
      return { success: true }
    }),

  /**
   * Log when a user clicks the Learning Hub nav link (especially external).
   * Creates an audit log entry so admins can see who accessed it.
   */
  logNavClick: protectedProcedure
    .input(z.object({ url: z.string() }))
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.auditLog.create({
        data: {
          userId: ctx.user.id,
          action: 'LEARNING_HUB_CLICK',
          entityType: 'LearningResource',
          detailsJson: { url: input.url, role: ctx.user.role },
        },
      }).catch(() => {})
      return { success: true }
    }),

  /**
   * Create a new resource (admin only)
   */
  create: adminProcedure
    .input(
      z.object({
        programId: z.string().nullable(),
        title: z.string().min(1).max(255),
        description: z.string().optional(),
        contentJson: z.any().optional(), // BlockNote document structure
        accessJson: accessRuleSchema.array().nullable().optional(),
        externalUrl: z.string().url().optional(),
        coverImageKey: z.string().optional(),
        sortOrder: z.number().int().default(0),
        isPublished: z.boolean().default(false),
        // File info (set after upload)
        fileName: z.string().optional(),
        mimeType: z.string().optional(),
        size: z.number().int().optional(),
        bucket: z.string().optional(),
        objectKey: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { accessJson, ...rest } = input

      const resource = await ctx.prisma.learningResource.create({
        data: {
          ...rest,
          accessJson: accessJson === null ? Prisma.JsonNull : accessJson ?? undefined,
          createdById: ctx.user.id,
        },
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'CREATE',
        entityType: 'LearningResource',
        entityId: resource.id,
        detailsJson: { title: input.title },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return resource
    }),

  /**
   * Update a resource (admin only)
   */
  update: adminProcedure
    .input(
      z.object({
        id: z.string(),
        title: z.string().min(1).max(255).optional(),
        description: z.string().optional().nullable(),
        contentJson: z.any().optional(), // BlockNote document structure
        accessJson: accessRuleSchema.array().nullable().optional(),
        externalUrl: z.string().url().optional().nullable(),
        coverImageKey: z.string().optional().nullable(),
        programId: z.string().nullable().optional(),
        sortOrder: z.number().int().optional(),
        isPublished: z.boolean().optional(),
        // File info (set after upload)
        fileName: z.string().optional(),
        mimeType: z.string().optional(),
        size: z.number().int().optional(),
        bucket: z.string().optional(),
        objectKey: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, accessJson, ...rest } = input

      // Prisma requires Prisma.JsonNull for nullable JSON fields instead of raw null
      const data = {
        ...rest,
        ...(accessJson !== undefined && {
          accessJson: accessJson === null ? Prisma.JsonNull : accessJson,
        }),
      }

      const resource = await ctx.prisma.learningResource.update({
        where: { id },
        data,
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'UPDATE',
        entityType: 'LearningResource',
        entityId: id,
        detailsJson: rest,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return resource
    }),

  /**
   * Delete a resource (admin only)
   */
  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const resource = await ctx.prisma.learningResource.delete({
        where: { id: input.id },
      })

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'DELETE',
        entityType: 'LearningResource',
        entityId: input.id,
        detailsJson: { title: resource.title },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return resource
    }),

  /**
   * Get upload URL for a resource file (admin only)
   */
  getUploadUrl: adminProcedure
    .input(
      z.object({
        fileName: z.string(),
        mimeType: z.string(),
      })
    )
    .mutation(async ({ input }) => {
      const timestamp = Date.now()
      const sanitizedName = input.fileName.replace(/[^a-zA-Z0-9.-]/g, '_')
      const objectKey = `resources/${timestamp}-${sanitizedName}`

      await ensureBucket(LEARNING_BUCKET)
      const url = await getPresignedUrl(LEARNING_BUCKET, objectKey, 'PUT', 3600)

      return {
        url,
        bucket: LEARNING_BUCKET,
        objectKey,
      }
    }),

  /**
   * Get access statistics for a resource (admin only)
   */
  getStats: adminProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const [totalViews, uniqueUsers, recentAccess] = await Promise.all([
        ctx.prisma.resourceAccess.count({
          where: { resourceId: input.id },
        }),
        ctx.prisma.resourceAccess.groupBy({
          by: ['userId'],
          where: { resourceId: input.id },
        }),
        ctx.prisma.resourceAccess.findMany({
          where: { resourceId: input.id },
          include: {
            user: { select: { id: true, name: true, email: true } },
          },
          orderBy: { accessedAt: 'desc' },
          take: 10,
        }),
      ])

      return {
        totalViews,
        uniqueUsers: uniqueUsers.length,
        recentAccess,
      }
    }),

  /**
   * Reorder resources (admin only)
   */
  reorder: adminProcedure
    .input(
      z.object({
        items: z.array(
          z.object({
            id: z.string(),
            sortOrder: z.number().int(),
          })
        ),
      })
    )
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.$transaction(
        input.items.map((item) =>
          ctx.prisma.learningResource.update({
            where: { id: item.id },
            data: { sortOrder: item.sortOrder },
          })
        )
      )

      // Audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: ctx.user.id,
        action: 'REORDER',
        entityType: 'LearningResource',
        detailsJson: { count: input.items.length },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      return { success: true }
    }),
})
