import { Currency, KycLevelClarification } from '@prisma/client'
import { z } from 'astro/zod'
import { ActionError } from 'astro:actions'
import { formatDistanceStrict } from 'date-fns'

import { captchaFormSchemaProperties, captchaFormSchemaSuperRefine } from '../lib/captchaValidation'
import { defineProtectedAction } from '../lib/defineProtectedAction'
import { saveFileLocally } from '../lib/fileStorage'
import { findServicesBySimilarity } from '../lib/findServicesBySimilarity'
import { handleHoneypotTrap } from '../lib/honeypot'
import { prisma } from '../lib/prisma'
import { separateServiceUrlsByType } from '../lib/urls'
import {
  imageFileSchemaRequired,
  stringListOfContactMethodsSchema,
  stringListOfUrlsSchemaRequired,
  zodCohercedNumber,
} from '../lib/zodUtils'

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

const SUGGESTION_MESSAGE_RATE_LIMIT_WINDOW_MINUTES = 1
const MAX_SUGGESTION_MESSAGES_PER_WINDOW = 5

export const SUGGESTION_NOTES_MAX_LENGTH = 1000
export const SUGGESTION_NAME_MAX_LENGTH = 20
export const SUGGESTION_SLUG_MAX_LENGTH = 20
export const SUGGESTION_DESCRIPTION_MAX_LENGTH = 100
export const SUGGESTION_MESSAGE_CONTENT_MAX_LENGTH = 1000

const findPossibleDuplicates = async (input: { name: string }) => {
  const matches = await findServicesBySimilarity(input.name, 0.3)

  return await prisma.service.findMany({
    where: {
      id: {
        in: matches.map(({ id }) => id),
      },
      serviceVisibility: {
        in: ['PUBLIC', 'ARCHIVED', 'UNLISTED'],
      },
    },
    select: {
      id: true,
      name: true,
      slug: true,
      description: true,
    },
  })
}

const serializeExtraNotes = async <T extends Record<string, unknown>, NK extends keyof T>(
  input: T,
  skipKeys: NK[] = [],
  mapKeys: { [P in Exclude<Extract<keyof T, string>, NK>]?: (value: T[P]) => unknown } = {}
): Promise<string> => {
  return (
    await Promise.all(
      Object.entries(input)
        .filter(
          (
            entry
          ): entry is [Exclude<Extract<keyof T, string>, NK>, T[Exclude<Extract<keyof T, string>, NK>]] =>
            !skipKeys.some((k) => k === entry[0])
        )
        .map(async ([key, originalValue]) => {
          const value = mapKeys[key] ? await mapKeys[key](originalValue) : originalValue
          let serializedValue = ''
          if (typeof value === 'string') {
            serializedValue = value
          } else if (value === undefined || value === null) {
            serializedValue = ''
          } else if (Array.isArray(value)) {
            serializedValue = value.map((item) => String(item)).join(', ')
          } else if (typeof value === 'object' && value instanceof Date) {
            serializedValue = value.toISOString()
          } else if (
            typeof value === 'object' &&
            'toString' in value &&
            typeof value.toString === 'function' &&
            // eslint-disable-next-line @typescript-eslint/no-base-to-string
            value.toString() !== '[object Object]'
          ) {
            // eslint-disable-next-line @typescript-eslint/no-base-to-string
            serializedValue = value.toString()
          } else {
            try {
              serializedValue = JSON.stringify(value)
            } catch (error) {
              serializedValue = `Error serializing value: ${error instanceof Error ? error.message : 'Unknown error'}`
            }
          }
          return `- ${key}: ${serializedValue}`
        })
    )
  ).join('\n')
}

export const serviceSuggestionActions = {
  editService: defineProtectedAction({
    accept: 'form',
    permissions: 'not-spammer',
    input: z
      .object({
        notes: z.string().max(SUGGESTION_NOTES_MAX_LENGTH).optional(),
        serviceId: z.coerce.number().int().positive(),
        extraNotes: z.string().optional(),
        /** @deprecated Honey pot field, do not use */
        message: z.unknown().optional(),
        ...captchaFormSchemaProperties,
      })
      .superRefine(captchaFormSchemaSuperRefine),
    handler: async (input, context) => {
      await handleHoneypotTrap({
        input,
        honeyPotTrapField: 'message',
        userId: context.locals.user.id,
        location: 'serviceSuggestion.editService',
      })

      const service = await prisma.service.findUnique({
        select: {
          id: true,
          slug: true,
        },
        where: { id: input.serviceId },
      })

      if (!service) {
        throw new ActionError({
          message: 'Service not found',
          code: 'BAD_REQUEST',
        })
      }

      // Combine notes and extraNotes if available
      const combinedNotes = input.extraNotes
        ? `${input.notes ?? ''}\n\nSuggested changes:\n${input.extraNotes}`
        : input.notes

      const serviceSuggestion = await prisma.serviceSuggestion.create({
        data: {
          type: 'EDIT_SERVICE',
          notes: combinedNotes,
          status: 'PENDING',
          userId: context.locals.user.id,
          serviceId: service.id,
        },
        select: {
          id: true,
        },
      })

      return { serviceSuggestion, service }
    },
  }),
  createService: defineProtectedAction({
    accept: 'form',
    permissions: 'not-spammer',
    input: z
      .object({
        notes: z.string().max(SUGGESTION_NOTES_MAX_LENGTH).optional(),
        name: z.string().min(1).max(SUGGESTION_NAME_MAX_LENGTH),
        slug: z
          .string()
          .min(1)
          .max(SUGGESTION_SLUG_MAX_LENGTH)
          .regex(/^[a-z0-9-]+$/, {
            message: 'Slug must contain only lowercase letters, numbers, and hyphens',
          }),
        description: z.string().min(1).max(SUGGESTION_DESCRIPTION_MAX_LENGTH),
        allServiceUrls: stringListOfUrlsSchemaRequired,
        tosUrls: stringListOfUrlsSchemaRequired,
        contactMethods: stringListOfContactMethodsSchema,
        kycLevel: zodCohercedNumber(z.coerce.number().int().min(0).max(4)),
        kycLevelClarification: z.nativeEnum(KycLevelClarification),
        attributes: z.array(z.coerce.number().int().positive()),
        categories: z.array(z.coerce.number().int().positive()).min(1),
        acceptedCurrencies: z.array(z.nativeEnum(Currency)).min(1),
        imageFile: imageFileSchemaRequired,
        rulesConfirm: z.literal('on', {
          errorMap: () => ({
            message: 'You must accept the suggestion rules and process to continue',
          }),
        }),
        operatingSince: z.coerce.date().optional(),
        /** @deprecated Honey pot field, do not use */
        message: z.unknown().optional(),
        skipDuplicateCheck: z
          .string()
          .optional()
          .nullable()
          .transform((value) => value === 'true'),
        ...captchaFormSchemaProperties,
      })
      .superRefine(captchaFormSchemaSuperRefine),

    handler: async (input, context) => {
      await handleHoneypotTrap({
        input,
        honeyPotTrapField: 'message',
        userId: context.locals.user.id,
        location: 'serviceSuggestion.createService',
      })

      const serviceWithSameSlug = await prisma.service.findUnique({
        select: { id: true, name: true, slug: true, description: true },
        where: { slug: input.slug },
      })

      if (!input.skipDuplicateCheck) {
        const possibleDuplicates = [
          ...(serviceWithSameSlug ? [serviceWithSameSlug] : []),
          ...(await findPossibleDuplicates(input)),
        ]

        if (possibleDuplicates.length > 0) {
          return {
            hasDuplicates: true,
            possibleDuplicates,
            extraNotes: await serializeExtraNotes(
              input,
              [
                'skipDuplicateCheck',
                'message',
                'imageFile',
                'captcha-value',
                'captcha-solution-hash',
                'rulesConfirm',
              ],
              {
                attributes: async (value) => {
                  const dbAttributes = await prisma.attribute.findMany({
                    select: {
                      title: true,
                    },
                    where: { id: { in: value } },
                  })
                  return dbAttributes.map((attribute) => `\n  - ${attribute.title}`).join('')
                },
                categories: async (value) => {
                  const dbCategories = await prisma.category.findMany({
                    select: {
                      name: true,
                    },
                    where: { id: { in: value } },
                  })
                  return dbCategories.map((category) => category.name)
                },
              }
            ),
            serviceSuggestion: undefined,
            service: undefined,
          } as const
        }
      } else {
        if (serviceWithSameSlug) {
          throw new ActionError({
            message: 'Slug already in use, try a different one',
            code: 'BAD_REQUEST',
          })
        }
      }

      const imageUrl = await saveFileLocally(input.imageFile, input.imageFile.name)

      const {
        web: serviceUrls,
        onion: onionUrls,
        i2p: i2pUrls,
      } = separateServiceUrlsByType(input.allServiceUrls)

      const { serviceSuggestion, service } = await prisma.$transaction(async (tx) => {
        const serviceSelect = {
          id: true,
          slug: true,
        } satisfies Prisma.ServiceSelect

        const service = await tx.service.create({
          data: {
            name: input.name,
            slug: input.slug,
            description: input.description,
            operatingSince: input.operatingSince,
            serviceUrls,
            tosUrls: input.tosUrls,
            onionUrls,
            i2pUrls,
            kycLevel: input.kycLevel,
            kycLevelClarification: input.kycLevelClarification,
            acceptedCurrencies: input.acceptedCurrencies,
            imageUrl,
            verificationStatus: 'COMMUNITY_CONTRIBUTED',
            overallScore: 0,
            privacyScore: 0,
            trustScore: 0,
            serviceVisibility: 'UNLISTED',
            categories: {
              connect: input.categories.map((id) => ({ id })),
            },
            attributes: {
              create: input.attributes.map((id) => ({
                attributeId: id,
              })),
            },
            contactMethods: {
              create: input.contactMethods.map((value) => ({
                value,
              })),
            },
          },
          select: serviceSelect,
        })

        const serviceSuggestion = await tx.serviceSuggestion.create({
          data: {
            notes: input.notes,
            type: 'CREATE_SERVICE',
            status: 'PENDING',
            userId: context.locals.user.id,
            serviceId: service.id,
          },
          select: {
            id: true,
          },
        })

        return {
          hasDuplicates: false,
          possibleDuplicates: [],
          extraNotes: undefined,
          serviceSuggestion,
          service,
        } as const
      })

      return {
        hasDuplicates: false,
        possibleDuplicates: [],
        extraNotes: undefined,
        serviceSuggestion,
        service,
      } as const
    },
  }),
  message: defineProtectedAction({
    accept: 'form',
    permissions: 'user',
    input: z.object({
      suggestionId: z.coerce.number().int().positive(),
      content: z.string().min(1).max(SUGGESTION_MESSAGE_CONTENT_MAX_LENGTH),
    }),
    handler: async (input, context) => {
      // --- Rate Limit Check Start --- (Admins are exempt)
      if (!context.locals.user.admin) {
        const windowStart = new Date(Date.now() - SUGGESTION_MESSAGE_RATE_LIMIT_WINDOW_MINUTES * 60 * 1000)
        const recentMessages = await prisma.serviceSuggestionMessage.findMany({
          where: {
            userId: context.locals.user.id,
            createdAt: {
              gte: windowStart,
            },
          },
          select: {
            id: true,
            createdAt: true,
          },
          orderBy: { createdAt: 'asc' }, // Get the oldest first to calculate wait time
        })

        if (recentMessages.length >= MAX_SUGGESTION_MESSAGES_PER_WINDOW) {
          const oldestMessageInWindow = recentMessages[0]
          if (!oldestMessageInWindow) {
            console.error(
              'Error determining oldest message for rate limit, but length check passed. User:',
              context.locals.user.id
            )
            throw new ActionError({
              code: 'INTERNAL_SERVER_ERROR',
              message: 'Could not determine rate limit window. Please try again.',
            })
          }
          const timeToWait = formatDistanceStrict(oldestMessageInWindow.createdAt, windowStart)
          console.warn(
            `Suggestion message rate limit exceeded for user ${context.locals.user.id.toLocaleString()}`
          )
          throw new ActionError({
            code: 'TOO_MANY_REQUESTS',
            message: `Rate limit exceeded. Please wait ${timeToWait} before sending another message.`,
          })
        }
      }
      // --- Rate Limit Check End ---

      const suggestion = await prisma.serviceSuggestion.findUnique({
        select: {
          id: true,
          userId: true,
        },
        where: { id: input.suggestionId },
      })

      if (!suggestion) {
        throw new ActionError({
          message: 'Suggestion not found',
          code: 'BAD_REQUEST',
        })
      }

      if (suggestion.userId !== context.locals.user.id) {
        throw new ActionError({
          message: 'Not authorized to send messages',
          code: 'UNAUTHORIZED',
        })
      }

      await prisma.serviceSuggestionMessage.create({
        data: {
          content: input.content,
          suggestionId: suggestion.id,
          userId: context.locals.user.id,
        },
      })
    },
  }),
}
