import crypto from 'crypto'

import { ActionError } from 'astro:actions'
import { z } from 'astro:schema'
import { formatDistanceStrict } from 'date-fns'

import { karmaUnlocksById } from '../constants/karmaUnlocks'
import { defineProtectedAction } from '../lib/defineProtectedAction'
import { handleHoneypotTrap } from '../lib/honeypot'
import { makeKarmaUnlockMessage } from '../lib/karmaUnlocks'
import { getOrCreateNotificationPreferences } from '../lib/notificationPreferences'
import { prisma } from '../lib/prisma'
import { timeTrapSecretKey } from '../lib/timeTrapSecret'

import type { CommentStatus, Prisma } from '@prisma/client'

const COMMENT_RATE_LIMIT_WINDOW_MINUTES = 2
const MAX_COMMENTS_PER_WINDOW = 1
const MAX_COMMENTS_PER_WINDOW_VERIFIED_USER = 10

export const commentActions = {
  vote: defineProtectedAction({
    accept: 'form',
    permissions: 'user',
    input: z.object({
      commentId: z.coerce.number().int().positive(),
      downvote: z.coerce.boolean(),
    }),
    handler: async (input, context) => {
      try {
        // Check user karma requirement
        if (!context.locals.user.karmaUnlocks.voteComments) {
          throw new ActionError({
            code: 'FORBIDDEN',
            message: makeKarmaUnlockMessage(karmaUnlocksById.voteComments),
          })
        }

        // Handle the vote in a transaction
        await prisma.$transaction(async (tx) => {
          // Get existing vote if any
          const existingVote = await tx.commentVote.findUnique({
            where: {
              commentId_userId: {
                commentId: input.commentId,
                userId: context.locals.user.id,
              },
            },
          })

          if (existingVote) {
            // If vote type is the same, remove the vote
            if (existingVote.downvote === input.downvote) {
              await tx.commentVote.delete({
                where: { id: existingVote.id },
              })
            } else {
              // If vote type is different, update the vote
              await tx.commentVote.update({
                where: { id: existingVote.id },
                data: { downvote: input.downvote },
              })
            }
          } else {
            // Create new vote
            await tx.commentVote.create({
              data: {
                downvote: input.downvote,
                commentId: input.commentId,
                userId: context.locals.user.id,
              },
            })
          }
        })

        return true
      } catch (error) {
        if (error instanceof ActionError) throw error

        console.error('Error voting on comment:', error)
        throw new ActionError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Error voting on comment',
        })
      }
    },
  }),

  create: defineProtectedAction({
    accept: 'form',
    permissions: 'user',
    input: z
      .object({
        content: z.string().min(10).max(2000),
        serviceId: z.coerce.number().int().positive(),
        parentId: z.coerce.number().optional(),
        /** @deprecated Honey pot field, do not use */
        message: z.unknown().optional(),
        rating: z.coerce.number().int().min(1).max(5).optional(),
        encTimestamp: z.string().min(1), // time trap field
        internalNote: z.string().max(500).optional(),
        issueKycRequested: z.coerce.boolean().optional(),
        issueFundsBlocked: z.coerce.boolean().optional(),
        issueScam: z.coerce.boolean().optional(),
        issueDetails: z.string().max(120).optional(),
        orderId: z.string().max(100).optional(),
      })
      .superRefine((data, ctx) => {
        if (data.rating && data.parentId) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            path: ['parentId'],
            message: 'Ratings cannot be provided for replies',
          })
        }
        if (!data.parentId) {
          if (data.content.length < 30) {
            ctx.addIssue({
              code: z.ZodIssueCode.too_small,
              minimum: 30,
              type: 'string',
              inclusive: true,
              path: ['content'],
              message: 'Content must be at least 30 characters',
            })
          }
        }
      }),
    handler: async (input, context) => {
      if (context.locals.user.karmaUnlocks.commentsDisabled) {
        throw new ActionError({
          code: 'FORBIDDEN',
          message: makeKarmaUnlockMessage(karmaUnlocksById.commentsDisabled),
        })
      }

      await handleHoneypotTrap({
        input,
        honeyPotTrapField: 'message',
        userId: context.locals.user.id,
        location: 'comment.create',
      })

      // --- Time Trap Validation Start ---
      try {
        const algorithm = 'aes-256-cbc'
        const decodedValue = Buffer.from(input.encTimestamp, 'base64').toString('utf8')
        const [ivHex, encryptedHex] = decodedValue.split(':')

        if (!ivHex || !encryptedHex) {
          throw new Error('Invalid time trap format.')
        }

        const iv = Buffer.from(ivHex, 'hex')
        const decipher = crypto.createDecipheriv(algorithm, timeTrapSecretKey, iv)
        let decrypted = decipher.update(encryptedHex, 'hex', 'utf8')
        decrypted += decipher.final('utf8')

        const originalTimestamp = parseInt(decrypted, 10)
        if (isNaN(originalTimestamp)) {
          throw new Error('Invalid timestamp data.')
        }

        const now = Date.now()
        const timeDiff = now - originalTimestamp
        const minTimeSeconds = 2 // 2 seconds
        const maxTimeMinutes = 60 // 1 hour

        if (timeDiff < minTimeSeconds * 1000 || timeDiff > maxTimeMinutes * 60 * 1000) {
          console.warn(`Time trap triggered: ${(timeDiff / 1000).toLocaleString()}s`)
          throw new Error('Invalid submission timing.')
        }
      } catch (err) {
        console.error('Time trap validation failed:', err instanceof Error ? err.message : 'Unknown error')
        throw new ActionError({
          code: 'BAD_REQUEST',
          message: 'Invalid request',
        })
      }
      // --- Time Trap Validation End ---

      // --- Rate Limit Check Start ---
      const isVerifiedUser = context.locals.user.admin || context.locals.user.verified
      const maxCommentsPerWindow = isVerifiedUser
        ? MAX_COMMENTS_PER_WINDOW_VERIFIED_USER
        : MAX_COMMENTS_PER_WINDOW

      const windowStart = new Date(Date.now() - COMMENT_RATE_LIMIT_WINDOW_MINUTES * 60 * 1000)
      const recentCommentCount = await prisma.comment.findMany({
        where: {
          authorId: context.locals.user.id,
          createdAt: {
            gte: windowStart,
          },
        },
        select: {
          id: true,
          createdAt: true,
        },
      })

      if (recentCommentCount.length >= maxCommentsPerWindow) {
        const oldestCreatedAt = recentCommentCount.reduce<Date | null>((oldestDate, comment) => {
          if (!oldestDate) return comment.createdAt
          if (comment.createdAt < oldestDate) return comment.createdAt
          return oldestDate
        }, null)

        console.warn(`Rate limit exceeded for user ${context.locals.user.id.toLocaleString()}`)
        throw new ActionError({
          code: 'TOO_MANY_REQUESTS', // Use specific 429 code
          message: `Rate limit exceeded. Please wait ${oldestCreatedAt ? `${formatDistanceStrict(oldestCreatedAt, windowStart)} ` : ''}before commenting again.`,
        })
      }
      // --- Rate Limit Check End ---

      // --- Format Internal Note from Issue Reports ---
      let formattedInternalNote: string | null = null
      // Track if this is an issue report
      const isIssueReport =
        input.issueKycRequested === true || input.issueFundsBlocked === true || input.issueScam === true

      if (isIssueReport) {
        const issueTypes = []
        if (input.issueKycRequested) issueTypes.push('KYC REQUESTED')
        if (input.issueFundsBlocked) issueTypes.push('FUNDS BLOCKED')
        if (input.issueScam) issueTypes.push('POTENTIAL SCAM')

        const details = input.issueDetails?.trim() ?? ''

        formattedInternalNote = `[${issueTypes.join(', ')}]${details ? `: ${details}` : ''}`
      } else if (input.internalNote?.trim()) {
        formattedInternalNote = input.internalNote.trim()
      }

      // Determine if admin review is needed (always true for issue reports)
      const requiresAdminReview = isIssueReport || !!(formattedInternalNote && !context.locals.user.admin)

      try {
        await prisma.$transaction(async (tx) => {
          // First deactivate any existing ratings if providing a new rating
          if (input.rating) {
            await tx.comment.updateMany({
              where: {
                serviceId: input.serviceId,
                authorId: context.locals.user.id,
                rating: { not: null },
              },
              data: {
                ratingActive: false,
              },
            })
          }

          // Check for existing orderId for this service if provided
          if (input.orderId?.trim()) {
            const existingOrderId = await tx.comment.findFirst({
              where: {
                serviceId: input.serviceId,
                orderId: input.orderId.trim(),
              },
              select: { id: true },
            })

            if (existingOrderId) {
              throw new ActionError({
                code: 'BAD_REQUEST',
                message: 'This Order ID has already been reported for this service.',
              })
            }
          }

          // Prepare data object with proper type safety
          const commentData: Prisma.CommentCreateInput = {
            content: input.content,
            service: { connect: { id: input.serviceId } },
            author: { connect: { id: context.locals.user.id } },

            // Change status to HUMAN_PENDING if there's an issue report, this is so that the AI worker does not pick it up for review
            status: context.locals.user.admin ? 'APPROVED' : isIssueReport ? 'HUMAN_PENDING' : 'PENDING',
            requiresAdminReview,
            orderId: input.orderId?.trim() ?? null,
            kycRequested: input.issueKycRequested === true,
            fundsBlocked: input.issueFundsBlocked === true,
          }

          if (input.parentId) {
            commentData.parent = { connect: { id: input.parentId } }
          }

          if (input.rating) {
            commentData.rating = input.rating
            commentData.ratingActive = true
          }

          if (formattedInternalNote) {
            commentData.internalNote = formattedInternalNote
          }

          const newComment = await tx.comment.create({
            data: commentData,
          })

          const notiPref = await getOrCreateNotificationPreferences(
            context.locals.user.id,
            { enableAutowatchMyComments: true },
            tx
          )

          if (notiPref.enableAutowatchMyComments) {
            await tx.notificationPreferences.update({
              where: { userId: context.locals.user.id },
              data: {
                watchedComments: { connect: { id: newComment.id } },
              },
            })
          }
        })

        return { success: true }
      } catch (error) {
        if (error instanceof ActionError) throw error

        console.error('Error creating comment:', error)
        throw new ActionError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Error creating comment',
        })
      }
    },
  }),

  moderate: defineProtectedAction({
    permissions: ['admin', 'verifier'],
    input: z.object({
      commentId: z.number(),
      userId: z.number(),
      action: z.enum([
        'status',
        'suspicious',
        'requires-admin-review',
        'community-note',
        'internal-note',
        'private-context',
        'order-id-status',
        'kyc-requested',
        'funds-blocked',
      ]),
      value: z.union([
        z.enum(['PENDING', 'APPROVED', 'VERIFIED', 'REJECTED']),
        z.enum(['PENDING', 'APPROVED', 'REJECTED']),
        z.boolean(),
        z.string(),
      ]),
    }),
    handler: async (input) => {
      try {
        const comment = await prisma.comment.findUnique({
          where: { id: input.commentId },
          select: {
            id: true,
            rating: true,
            serviceId: true,
            createdAt: true,
            authorId: true,
          },
        })

        if (!comment) {
          throw new ActionError({
            code: 'NOT_FOUND',
            message: 'Comment not found',
          })
        }

        const updateData: Prisma.CommentUpdateInput = {}

        switch (input.action) {
          case 'status':
            updateData.status = input.value as CommentStatus
            break
          case 'suspicious': {
            const isSpam = !!input.value
            updateData.suspicious = isSpam
            updateData.ratingActive = false

            if (!isSpam && comment.rating) {
              const newestRatingOrActiveRating = await prisma.comment.findFirst({
                where: {
                  serviceId: comment.serviceId,
                  authorId: comment.authorId,
                  id: { not: input.commentId },
                  rating: { not: null },
                  OR: [{ createdAt: { gt: comment.createdAt } }, { ratingActive: true }],
                },
              })
              updateData.ratingActive = !newestRatingOrActiveRating
            }
            break
          }
          case 'requires-admin-review':
            updateData.requiresAdminReview = !!input.value
            break
          case 'community-note':
            updateData.communityNote = input.value as string
            break
          case 'internal-note':
            updateData.internalNote = input.value as string
            break
          case 'private-context':
            updateData.privateContext = input.value as string
            break
          case 'order-id-status':
            updateData.orderIdStatus = input.value as 'APPROVED' | 'PENDING' | 'REJECTED'
            break
          case 'kyc-requested':
            updateData.kycRequested = !!input.value
            break
          case 'funds-blocked':
            updateData.fundsBlocked = !!input.value
            break
        }

        // Update the comment
        await prisma.comment.update({
          where: { id: input.commentId },
          data: updateData,
        })

        return { success: true }
      } catch (error) {
        if (error instanceof ActionError) throw error

        console.error('Error moderating comment:', error)
        throw new ActionError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Error moderating comment',
        })
      }
    },
  }),
}
