import { z } from 'zod'
import { TRPCError } from '@trpc/server'
import { router, publicProcedure } from '../trpc'
import { Prisma, CompetitionCategory, OceanIssue, TeamMemberRole } from '@prisma/client'
import {
  createNotification,
  notifyAdmins,
  notifyProjectTeam,
  NotificationTypes,
} from '../services/in-app-notification'
import { checkRateLimit } from '@/lib/rate-limit'
import { logAudit } from '@/server/utils/audit'
import { parseWizardConfig } from '@/lib/wizard-config'

// Zod schemas for the application form
const teamMemberSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email address'),
  role: z.nativeEnum(TeamMemberRole).default('MEMBER'),
  title: z.string().optional(),
})

const applicationSchema = z.object({
  // Step 1: Category (string to support admin-configured custom values)
  competitionCategory: z.string().min(1, 'Competition category is required'),

  // Step 2: Contact Info
  contactName: z.string().min(2, 'Full name is required'),
  contactEmail: z.string().email('Invalid email address'),
  contactPhone: z.string().min(5, 'Phone number is required'),
  country: z.string().min(2, 'Country is required'),
  city: z.string().optional(),

  // Step 3: Project Details (string to support admin-configured custom values)
  projectName: z.string().min(2, 'Project name is required').max(200),
  teamName: z.string().optional(),
  description: z.string().min(20, 'Description must be at least 20 characters'),
  oceanIssue: z.string().min(1, 'Ocean issue is required'),

  // Step 4: Team Members
  teamMembers: z.array(teamMemberSchema).optional(),

  // Step 5: Additional Info (conditional & optional)
  institution: z.string().optional(), // Required if BUSINESS_CONCEPT
  startupCreatedDate: z.string().optional(), // Required if STARTUP
  wantsMentorship: z.boolean().default(false),
  referralSource: z.string().optional(),

  // Consent
  gdprConsent: z.boolean().refine((val) => val === true, {
    message: 'You must agree to the data processing terms',
  }),
})

// Passthrough version for tRPC input (allows custom fields to pass through)
const applicationInputSchema = applicationSchema.passthrough()

export type ApplicationFormData = z.infer<typeof applicationSchema>

// Known core field names that are stored in dedicated columns (not custom fields)
const CORE_FIELD_NAMES = new Set([
  'competitionCategory', 'contactName', 'contactEmail', 'contactPhone',
  'country', 'city', 'projectName', 'teamName', 'description', 'oceanIssue',
  'teamMembers', 'institution', 'startupCreatedDate', 'wantsMentorship',
  'referralSource', 'gdprConsent',
])

/**
 * Extract custom field values from form data based on wizard config.
 * Returns an object with { customFields: { fieldId: value } } if any custom fields exist.
 */
function extractCustomFieldData(
  settingsJson: unknown,
  formData: Record<string, unknown>
): Record<string, unknown> {
  const config = parseWizardConfig(settingsJson)
  if (!config.customFields?.length) return {}

  const customFieldData: Record<string, unknown> = {}
  for (const field of config.customFields) {
    const value = formData[field.id as keyof typeof formData]
    if (value !== undefined && value !== '' && value !== null) {
      customFieldData[field.id] = value
    }
  }

  if (Object.keys(customFieldData).length === 0) return {}
  return { customFields: customFieldData }
}

export const applicationRouter = router({
  /**
   * Get application configuration for a round or edition
   */
  getConfig: publicProcedure
    .input(
      z.object({
        slug: z.string(),
        mode: z.enum(['edition', 'stage']).default('stage'),
      })
    )
    .query(async ({ ctx, input }) => {
      const now = new Date()

      if (input.mode === 'edition') {
        // Edition-wide application mode
        const program = await ctx.prisma.program.findFirst({
          where: { slug: input.slug },
        })

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

        // Check if program supports edition-wide applications
        const settingsJson = (program.settingsJson || {}) as Record<string, unknown>
        const applyMode = (settingsJson.applyMode as string) || 'round'

        if (applyMode !== 'edition') {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'This program does not support edition-wide applications',
          })
        }

        // Check if applications are open (based on program dates)
        const submissionStartDate = settingsJson.submissionStartDate
          ? new Date(settingsJson.submissionStartDate as string)
          : null
        const submissionEndDate = settingsJson.submissionEndDate
          ? new Date(settingsJson.submissionEndDate as string)
          : null

        let isOpen = false
        let gracePeriodEnd: Date | null = null

        if (submissionStartDate && submissionEndDate) {
          isOpen = now >= submissionStartDate && now <= submissionEndDate

          // Check grace period
          const lateSubmissionGrace = settingsJson.lateSubmissionGrace as number | undefined
          if (!isOpen && lateSubmissionGrace) {
            gracePeriodEnd = new Date(submissionEndDate.getTime() + lateSubmissionGrace * 60 * 60 * 1000)
            isOpen = now <= gracePeriodEnd
          }
        } else {
          isOpen = program.status === 'ACTIVE'
        }

        // Block access entirely when applications are not open
        if (!isOpen) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'Applications are not currently open. The submission period has not started or has ended.',
          })
        }

        const wizardConfig = parseWizardConfig(program.settingsJson)

        return {
          mode: 'edition' as const,
          program: {
            id: program.id,
            name: program.name,
            year: program.year,
            description: program.description,
            slug: program.slug,
            submissionStartDate,
            submissionEndDate,
            gracePeriodEnd,
            isOpen,
          },
          wizardConfig,
          oceanIssueOptions: wizardConfig.oceanIssues ?? [],
          competitionCategories: wizardConfig.competitionCategories ?? [],
        }
      } else {
        // Round-specific application mode (backward compatible with round slug)
        const round = await ctx.prisma.round.findFirst({
          where: { slug: input.slug },
          include: {
            competition: {
              include: {
                program: {
                  select: {
                    id: true,
                    name: true,
                    year: true,
                    description: true,
                    settingsJson: true,
                  },
                },
              },
            },
          },
        })

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

        const roundProgram = round.competition.program
        const isOpen = round.status === 'ROUND_ACTIVE'

        // Block access entirely when the intake round is not active
        if (!isOpen) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'Applications are not currently open. The submission period has not started or has ended.',
          })
        }

        const roundWizardConfig = parseWizardConfig(roundProgram.settingsJson)
        const { settingsJson: _s, ...programData } = roundProgram

        return {
          mode: 'stage' as const,
          stage: {
            id: round.id,
            name: round.name,
            slug: round.slug,
            submissionStartDate: null,
            submissionEndDate: null,
            submissionDeadline: null,
            lateSubmissionGrace: null,
            gracePeriodEnd: null,
            isOpen,
          },
          program: programData,
          wizardConfig: roundWizardConfig,
          oceanIssueOptions: roundWizardConfig.oceanIssues ?? [],
          competitionCategories: roundWizardConfig.competitionCategories ?? [],
        }
      }
    }),

  /**
   * Submit a new application (edition-wide or round-specific)
   */
  submit: publicProcedure
    .input(
      z.object({
        mode: z.enum(['edition', 'stage']).default('stage'),
        programId: z.string().optional(),
        roundId: z.string().optional(),
        data: applicationInputSchema,
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Stricter rate limit for application submissions: 5 per hour per IP
      const ip = ctx.ip || 'unknown'
      const submitRateLimit = checkRateLimit(`app-submit:${ip}`, 5, 60 * 60 * 1000)
      if (!submitRateLimit.success) {
        throw new TRPCError({
          code: 'TOO_MANY_REQUESTS',
          message: 'Too many application submissions. Please try again later.',
        })
      }

      const { mode, programId, roundId, data } = input

      // Validate input based on mode
      if (mode === 'edition' && !programId) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'programId is required for edition-wide applications',
        })
      }

      if (mode === 'stage' && !roundId) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'roundId is required for round-specific applications',
        })
      }

      const now = new Date()
      let program: { id: string; name: string; year: number; status: string; settingsJson?: unknown }
      let isOpen = false

      if (mode === 'edition') {
        // Edition-wide application
        program = await ctx.prisma.program.findUniqueOrThrow({
          where: { id: programId },
          select: {
            id: true,
            name: true,
            year: true,
            status: true,
            settingsJson: true,
          },
        })

        // Check if program supports edition-wide applications
        const settingsJson = (program.settingsJson || {}) as Record<string, unknown>
        const applyMode = (settingsJson.applyMode as string) || 'round'

        if (applyMode !== 'edition') {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'This program does not support edition-wide applications',
          })
        }

        // Check submission window
        const submissionStartDate = settingsJson.submissionStartDate
          ? new Date(settingsJson.submissionStartDate as string)
          : null
        const submissionEndDate = settingsJson.submissionEndDate
          ? new Date(settingsJson.submissionEndDate as string)
          : null

        if (submissionStartDate && submissionEndDate) {
          isOpen = now >= submissionStartDate && now <= submissionEndDate

          // Check grace period
          const lateSubmissionGrace = settingsJson.lateSubmissionGrace as number | undefined
          if (!isOpen && lateSubmissionGrace) {
            const gracePeriodEnd = new Date(submissionEndDate.getTime() + lateSubmissionGrace * 60 * 60 * 1000)
            isOpen = now <= gracePeriodEnd
          }
        } else {
          isOpen = program.status === 'ACTIVE'
        }

        if (!isOpen) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Applications are currently closed for this edition',
          })
        }

        // Check if email already submitted for this edition
        const existingProject = await ctx.prisma.project.findFirst({
          where: {
            programId,
            submittedByEmail: data.contactEmail,
          },
        })

        if (existingProject) {
          throw new TRPCError({
            code: 'CONFLICT',
            message: 'An application with this email already exists for this edition',
          })
        }
      } else {
        // Round-specific application
        const round = await ctx.prisma.round.findUniqueOrThrow({
          where: { id: roundId! },
          include: {
            competition: {
              include: {
                program: true,
              },
            },
          },
        })

        program = round.competition.program

        // Check submission window
        isOpen = round.status === 'ROUND_ACTIVE'

        if (!isOpen) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Applications are currently closed for this round',
          })
        }

        // Check if email already submitted for this round
        const existingProject = await ctx.prisma.project.findFirst({
          where: {
            programId: program.id,
            submittedByEmail: data.contactEmail,
          },
        })

        if (existingProject) {
          throw new TRPCError({
            code: 'CONFLICT',
            message: 'An application with this email already exists for this round',
          })
        }
      }

      // Check if user exists, or create a new applicant user
      let user = await ctx.prisma.user.findUnique({
        where: { email: data.contactEmail },
      })

      if (!user) {
        user = await ctx.prisma.user.create({
          data: {
            email: data.contactEmail,
            name: data.contactName,
            role: 'APPLICANT',
            roles: ['APPLICANT'],
            status: 'ACTIVE',
            phoneNumber: data.contactPhone,
          },
        })
      }

      // Map string values to Prisma enums (safe for admin-configured custom values)
      const validCategories = Object.values(CompetitionCategory) as string[]
      const validOceanIssues = Object.values(OceanIssue) as string[]
      const categoryEnum = validCategories.includes(data.competitionCategory)
        ? (data.competitionCategory as CompetitionCategory)
        : null
      const oceanIssueEnum = validOceanIssues.includes(data.oceanIssue)
        ? (data.oceanIssue as OceanIssue)
        : null

      // Create the project
      const project = await ctx.prisma.project.create({
        data: {
          programId: program.id,
          title: data.projectName,
          teamName: data.teamName,
          description: data.description,
          competitionCategory: categoryEnum,
          oceanIssue: oceanIssueEnum,
          country: data.country,
          geographicZone: data.city ? `${data.city}, ${data.country}` : data.country,
          institution: data.institution,
          wantsMentorship: data.wantsMentorship,
          referralSource: data.referralSource,
          submissionSource: 'PUBLIC_FORM',
          submittedByEmail: data.contactEmail,
          submittedByUserId: user.id,
          submittedAt: now,
          metadataJson: {
            contactPhone: data.contactPhone,
            startupCreatedDate: data.startupCreatedDate,
            gdprConsentAt: now.toISOString(),
            applicationMode: mode,
            // Store raw string values for custom categories/issues
            ...(categoryEnum ? {} : { competitionCategoryRaw: data.competitionCategory }),
            ...(oceanIssueEnum ? {} : { oceanIssueRaw: data.oceanIssue }),
            // Store custom field values from wizard config
            ...extractCustomFieldData(program.settingsJson, data),
          },
        },
      })

      // Create team lead membership
      await ctx.prisma.teamMember.create({
        data: {
          projectId: project.id,
          userId: user.id,
          role: 'LEAD',
          title: 'Team Lead',
        },
      })

      // Create additional team members
      if (data.teamMembers && data.teamMembers.length > 0) {
        for (const member of data.teamMembers) {
          // Find or create user for team member
          let memberUser = await ctx.prisma.user.findUnique({
            where: { email: member.email },
          })

          if (!memberUser) {
            memberUser = await ctx.prisma.user.create({
              data: {
                email: member.email,
                name: member.name,
                role: 'APPLICANT',
                roles: ['APPLICANT'],
                status: 'NONE',
              },
            })
          }

          // Create team membership
          await ctx.prisma.teamMember.create({
            data: {
              projectId: project.id,
              userId: memberUser.id,
              role: member.role,
              title: member.title,
            },
          })
        }
      }

      // Create audit log
      await logAudit({
        prisma: ctx.prisma,
        userId: user.id,
        action: 'CREATE',
        entityType: 'Project',
        entityId: project.id,
        detailsJson: {
          source: 'public_application_form',
          title: data.projectName,
          category: data.competitionCategory,
          autoAssignedStage: null,
        },
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      })

      // Notify applicant of successful submission
      await createNotification({
        userId: user.id,
        type: NotificationTypes.APPLICATION_SUBMITTED,
        title: 'Application Received',
        message: `Your application for "${data.projectName}" has been successfully submitted to ${program.name}.`,
        linkUrl: `/team/projects/${project.id}`,
        linkLabel: 'View Application',
        metadata: {
          projectName: data.projectName,
          programName: program.name,
        },
      })

      // Notify admins of new application
      await notifyAdmins({
        type: NotificationTypes.NEW_APPLICATION,
        title: 'New Application',
        message: `New application received: "${data.projectName}" from ${data.contactName}.`,
        linkUrl: `/admin/projects/${project.id}`,
        linkLabel: 'Review Application',
        metadata: {
          projectName: data.projectName,
          applicantName: data.contactName,
          applicantEmail: data.contactEmail,
          programName: program.name,
        },
      })

      return {
        success: true,
        projectId: project.id,
        message: `Thank you for applying to ${program.name} ${program.year}! We will review your application and contact you at ${data.contactEmail}.`,
      }
    }),

  /**
   * Check if email is already registered for a round or edition
   */
  checkEmailAvailability: publicProcedure
    .input(
      z.object({
        mode: z.enum(['edition', 'stage']).default('stage'),
        programId: z.string().optional(),
        roundId: z.string().optional(),
        email: z.string().email(),
      })
    )
    .query(async ({ ctx, input }) => {
      // Rate limit to prevent email enumeration
      const ip = ctx.ip || 'unknown'
      const emailCheckLimit = checkRateLimit(`email-check:${ip}`, 20, 15 * 60 * 1000)
      if (!emailCheckLimit.success) {
        throw new TRPCError({
          code: 'TOO_MANY_REQUESTS',
          message: 'Too many requests. Please try again later.',
        })
      }

      let existing
      if (input.mode === 'edition') {
        existing = await ctx.prisma.project.findFirst({
          where: {
            programId: input.programId,
            submittedByEmail: input.email,
          },
        })
      } else {
        // For round-specific applications, check by program (derived from round)
        if (input.roundId) {
          const round = await ctx.prisma.round.findUnique({
            where: { id: input.roundId },
            select: { competition: { select: { programId: true } } },
          })
          if (round) {
            existing = await ctx.prisma.project.findFirst({
              where: {
                programId: round.competition.programId,
                submittedByEmail: input.email,
              },
            })
          }
        }
      }

      return {
        available: !existing,
        message: existing
          ? `An application with this email already exists for this ${input.mode === 'edition' ? 'edition' : 'stage'}`
          : null,
      }
    }),

  // =========================================================================
  // Draft Saving & Resume (F11)
  // =========================================================================

  /**
   * Save application as draft with resume token
   */
  saveDraft: publicProcedure
    .input(
      z.object({
        roundSlug: z.string(),
        programId: z.string().optional(),
        email: z.string().email(),
        draftDataJson: z.record(z.unknown()),
        title: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Find round by slug
      const round = await ctx.prisma.round.findFirst({
        where: { slug: input.roundSlug },
        select: {
          id: true,
          status: true,
          competition: { select: { programId: true } },
          configJson: true,
        },
      })

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

      // Block draft saving when intake round is not active
      if (round.status !== 'ROUND_ACTIVE') {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Applications are not currently open.',
        })
      }

      const roundConfig = (round.configJson as Record<string, unknown>) || {}
      if (roundConfig.drafts_enabled === false) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Draft saving is not enabled for this round',
        })
      }

      const draftExpiryDays = (roundConfig.draft_expiry_days as number) || 30
      const draftExpiresAt = new Date()
      draftExpiresAt.setDate(draftExpiresAt.getDate() + draftExpiryDays)

      const draftToken = `draft_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`

      const programId = input.programId || round.competition.programId

      const existingDraft = await ctx.prisma.project.findFirst({
        where: {
          programId,
          submittedByEmail: input.email,
          isDraft: true,
        },
      })

      if (existingDraft) {
        const updated = await ctx.prisma.project.update({
          where: { id: existingDraft.id },
          data: {
            title: input.title || existingDraft.title,
            draftDataJson: input.draftDataJson as Prisma.InputJsonValue,
            draftExpiresAt,
            metadataJson: {
              ...((existingDraft.metadataJson as Record<string, unknown>) || {}),
              draftToken,
            } as Prisma.InputJsonValue,
          },
        })

        return { projectId: updated.id, draftToken }
      }

      const project = await ctx.prisma.project.create({
        data: {
          programId,
          title: input.title || 'Untitled Draft',
          isDraft: true,
          draftDataJson: input.draftDataJson as Prisma.InputJsonValue,
          draftExpiresAt,
          submittedByEmail: input.email,
          metadataJson: {
            draftToken,
          },
        },
      })

      return { projectId: project.id, draftToken }
    }),

  /**
   * Resume a draft application using a token
   */
  resumeDraft: publicProcedure
    .input(z.object({ draftToken: z.string() }))
    .query(async ({ ctx, input }) => {
      const projects = await ctx.prisma.project.findMany({
        where: {
          isDraft: true,
        },
      })

      // Find project with matching token in metadataJson
      const project = projects.find((p) => {
        const metadata = p.metadataJson as Record<string, unknown> | null
        return metadata?.draftToken === input.draftToken
      })

      if (!project) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Draft not found or invalid token',
        })
      }

      // Check expiry
      if (project.draftExpiresAt && new Date() > project.draftExpiresAt) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'This draft has expired',
        })
      }

      return {
        projectId: project.id,
        draftDataJson: project.draftDataJson,
        title: project.title,
      }
    }),

  /**
   * Submit a saved draft as a final application
   */
  submitDraft: publicProcedure
    .input(
      z.object({
        projectId: z.string(),
        draftToken: z.string(),
        data: applicationSchema,
      })
    )
    .mutation(async ({ ctx, input }) => {
      const project = await ctx.prisma.project.findUniqueOrThrow({
        where: { id: input.projectId },
        include: { program: true },
      })

      // Verify token
      const metadata = (project.metadataJson as Record<string, unknown>) || {}
      if (metadata.draftToken !== input.draftToken) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Invalid draft token',
        })
      }

      if (!project.isDraft) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'This project has already been submitted',
        })
      }

      const now = new Date()
      const { data } = input

      // Find or create user
      let user = await ctx.prisma.user.findUnique({
        where: { email: data.contactEmail },
      })

      if (!user) {
        user = await ctx.prisma.user.create({
          data: {
            email: data.contactEmail,
            name: data.contactName,
            role: 'APPLICANT',
            roles: ['APPLICANT'],
            status: 'ACTIVE',
            phoneNumber: data.contactPhone,
          },
        })
      }

      // Update project with final data
      const updated = await ctx.prisma.project.update({
        where: { id: input.projectId },
        data: {
          isDraft: false,
          draftDataJson: Prisma.DbNull,
          draftExpiresAt: null,
          title: data.projectName,
          teamName: data.teamName,
          description: data.description,
          competitionCategory: data.competitionCategory as CompetitionCategory,
          oceanIssue: data.oceanIssue as OceanIssue,
          country: data.country,
          geographicZone: data.city ? `${data.city}, ${data.country}` : data.country,
          institution: data.institution,
          wantsMentorship: data.wantsMentorship,
          referralSource: data.referralSource,
          submissionSource: 'PUBLIC_FORM',
          submittedByEmail: data.contactEmail,
          submittedByUserId: user.id,
          submittedAt: now,
          status: 'SUBMITTED',
          metadataJson: {
            contactPhone: data.contactPhone,
            startupCreatedDate: data.startupCreatedDate,
            gdprConsentAt: now.toISOString(),
          },
        },
      })

      // Audit log
      try {
        await logAudit({
          prisma: ctx.prisma,
          userId: user.id,
          action: 'DRAFT_SUBMITTED',
          entityType: 'Project',
          entityId: updated.id,
          detailsJson: {
            source: 'draft_submission',
            title: data.projectName,
            category: data.competitionCategory,
          },
          ipAddress: ctx.ip,
          userAgent: ctx.userAgent,
        })
      } catch (err) {
        console.error('Failed to write audit log for draft submission:', err)
        // Never throw on audit failure
      }

      return {
        success: true,
        projectId: updated.id,
        message: `Thank you for applying to ${project.program?.name ?? 'the program'}!`,
      }
    }),

  /**
   * Get a read-only preview of draft data
   */
  getPreview: publicProcedure
    .input(z.object({ draftToken: z.string() }))
    .query(async ({ ctx, input }) => {
      const projects = await ctx.prisma.project.findMany({
        where: {
          isDraft: true,
        },
      })

      const project = projects.find((p) => {
        const metadata = p.metadataJson as Record<string, unknown> | null
        return metadata?.draftToken === input.draftToken
      })

      if (!project) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Draft not found or invalid token',
        })
      }

      return {
        title: project.title,
        draftDataJson: project.draftDataJson,
        createdAt: project.createdAt,
        expiresAt: project.draftExpiresAt,
      }
    }),
})
