import { initTRPC, TRPCError } from '@trpc/server'
import superjson from 'superjson'
import { ZodError } from 'zod'
import type { Prisma } from '@prisma/client'
import type { Context } from './context'
import type { UserRole } from '@prisma/client'
import { checkRateLimit } from '@/lib/rate-limit'

/**
 * Initialize tRPC with context type and configuration
 */
const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    }
  },
})

/**
 * Export reusable router and procedure helpers
 */
export const router = t.router
export const publicProcedure = t.procedure
export const middleware = t.middleware
export const createCallerFactory = t.createCallerFactory

// =============================================================================
// Middleware
// =============================================================================

/**
 * Middleware to require authenticated user
 */
const isAuthenticated = middleware(async ({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'You must be logged in to perform this action',
    })
  }

  return next({
    ctx: {
      ...ctx,
      user: ctx.session.user,
    },
  })
})

/**
 * Helper to check if a user has any of the specified roles.
 * Checks the roles array first, falls back to [role] for stale JWT tokens.
 */
export function userHasRole(user: { role: UserRole; roles?: UserRole[] }, ...checkRoles: UserRole[]): boolean {
  const userRoles = user.roles?.length ? user.roles : [user.role]
  return checkRoles.some(r => userRoles.includes(r))
}

/**
 * Middleware to require specific role(s)
 */
const hasRole = (...roles: UserRole[]) =>
  middleware(async ({ ctx, next }) => {
    if (!ctx.session?.user) {
      throw new TRPCError({
        code: 'UNAUTHORIZED',
        message: 'You must be logged in to perform this action',
      })
    }

    // Use roles array, fallback to [role] for stale JWT tokens
    const userRoles = ctx.session.user.roles?.length
      ? ctx.session.user.roles
      : [ctx.session.user.role]

    if (!roles.some(r => userRoles.includes(r))) {
      throw new TRPCError({
        code: 'FORBIDDEN',
        message: 'You do not have permission to perform this action',
      })
    }

    return next({
      ctx: {
        ...ctx,
        user: ctx.session.user,
      },
    })
  })

// =============================================================================
// Mutation Audit Logging
// =============================================================================

/** Fields that must never appear in audit log input snapshots. */
const SENSITIVE_KEYS = new Set([
  'password', 'passwordHash', 'currentPassword', 'newPassword', 'confirmPassword',
  'token', 'secret', 'apiKey', 'accessKey', 'secretKey',
  'creditCard', 'cvv', 'ssn',
])

/** Max depth / size for serialized input to avoid bloating the audit table. */
const MAX_INPUT_DEPTH = 4
const MAX_STRING_LENGTH = 500
const MAX_ARRAY_LENGTH = 20

/**
 * Recursively sanitize an input object for safe storage in audit logs.
 * - Strips sensitive fields (passwords, tokens, secrets)
 * - Truncates long strings and arrays
 * - Limits nesting depth
 */
function sanitizeInput(value: unknown, depth = 0): unknown {
  if (depth > MAX_INPUT_DEPTH) return '[nested]'
  if (value === null || value === undefined) return value
  if (typeof value === 'boolean' || typeof value === 'number') return value
  if (typeof value === 'string') {
    return value.length > MAX_STRING_LENGTH
      ? value.slice(0, MAX_STRING_LENGTH) + '...'
      : value
  }
  if (value instanceof Date) return value.toISOString()
  if (Array.isArray(value)) {
    const truncated = value.slice(0, MAX_ARRAY_LENGTH).map(v => sanitizeInput(v, depth + 1))
    if (value.length > MAX_ARRAY_LENGTH) truncated.push(`[+${value.length - MAX_ARRAY_LENGTH} more]`)
    return truncated
  }
  if (typeof value === 'object') {
    const result: Record<string, unknown> = {}
    for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
      if (SENSITIVE_KEYS.has(key)) {
        result[key] = '[REDACTED]'
      } else {
        result[key] = sanitizeInput(val, depth + 1)
      }
    }
    return result
  }
  return String(value)
}

/**
 * Extract common fields from a sanitized input object for audit logging.
 */
function extractAuditFields(path: string, sanitizedInput: unknown) {
  const dotIndex = path.indexOf('.')
  const routerName = dotIndex > 0 ? path.slice(0, dotIndex) : path
  const procedureName = dotIndex > 0 ? path.slice(dotIndex + 1) : path

  // Convert procedure path to readable action (e.g., "evaluation.submit" → "EVALUATION_SUBMIT")
  const action = path.replace(/\./g, '_').replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()

  // Capitalize first letter of router name for entityType
  const entityType = routerName.charAt(0).toUpperCase() + routerName.slice(1)

  // Try to extract entityId from common input patterns
  const inputObj = (typeof sanitizedInput === 'object' && sanitizedInput !== null)
    ? sanitizedInput as Record<string, unknown>
    : undefined
  const entityId = inputObj?.id ?? inputObj?.userId ?? inputObj?.projectId ??
    inputObj?.roundId ?? inputObj?.competitionId ?? inputObj?.editionId ??
    inputObj?.targetUserId ?? inputObj?.sessionId ?? inputObj?.awardId

  return { routerName, procedureName, action, entityType, entityId }
}

/**
 * Middleware that automatically logs all successful mutations for non-SUPER_ADMIN users.
 * During impersonation, logs both the target user ID and the real admin ID.
 * Captures: procedure path, sanitized input, user role, IP, user agent.
 * Failures are silently caught — audit logging never breaks the calling operation.
 */
const withMutationAudit = middleware(async ({ ctx, next, path, type, getRawInput }) => {
  const result = await next()

  // Only log mutations, only on success
  if (type !== 'mutation' || !result.ok) return result

  // Must have an authenticated user
  const user = ctx.session?.user
  if (!user?.id) return result

  // Skip SUPER_ADMIN (unless impersonating — then user.role is the target's role)
  const impersonation = user.impersonating as { originalId: string; originalRole: string; originalEmail: string } | undefined
  if (user.role === 'SUPER_ADMIN' && !impersonation) return result

  try {
    // Get and sanitize the raw input
    let sanitizedInput: unknown = undefined
    try {
      const rawInput = await getRawInput()
      if (rawInput !== undefined) {
        sanitizedInput = sanitizeInput(rawInput)
      }
    } catch {
      // getRawInput can fail if input was already consumed; ignore
    }

    const { procedureName, action, entityType, entityId } = extractAuditFields(path, sanitizedInput)

    // Build details payload — include impersonation info when active
    const details: Record<string, unknown> = {
      procedure: path,
      procedureName,
      role: user.role,
      roles: user.roles,
      input: sanitizedInput,
    }
    if (impersonation) {
      details.impersonatedBy = {
        adminId: impersonation.originalId,
        adminEmail: impersonation.originalEmail,
        adminRole: impersonation.originalRole,
      }
    }

    await ctx.prisma.auditLog.create({
      data: {
        // During impersonation, log as the real admin with target info in details
        userId: impersonation ? impersonation.originalId : user.id,
        action: impersonation ? `IMPERSONATED_${action}` : action,
        entityType,
        entityId: entityId ? String(entityId) : undefined,
        detailsJson: details as Prisma.InputJsonValue,
        ipAddress: ctx.ip,
        userAgent: ctx.userAgent,
      },
    })
  } catch (error) {
    // Never break the calling operation on audit failure
    console.error('[MutationAudit] Failed to log:', path, error)
  }

  return result
})

/**
 * Middleware that logs failed operations (FORBIDDEN, UNAUTHORIZED, NOT_FOUND errors).
 * Tracks permission denials and access violations per user for security monitoring.
 * Applied to all authenticated procedures.
 */
const withErrorAudit = middleware(async ({ ctx, next, path, type, getRawInput }) => {
  try {
    return await next()
  } catch (error) {
    // Only log TRPCErrors that indicate security/access issues
    if (error instanceof TRPCError) {
      const securityCodes = ['FORBIDDEN', 'UNAUTHORIZED', 'NOT_FOUND'] as const
      if (securityCodes.includes(error.code as typeof securityCodes[number])) {
        try {
          let sanitizedInput: unknown = undefined
          try {
            const rawInput = await getRawInput()
            if (rawInput !== undefined) {
              sanitizedInput = sanitizeInput(rawInput)
            }
          } catch { /* ignore */ }

          const { procedureName, entityType, entityId } = extractAuditFields(path, sanitizedInput)

          const user = ctx.session?.user
          const impersonation = user?.impersonating as { originalId: string; originalEmail: string } | undefined

          await ctx.prisma.auditLog.create({
            data: {
              userId: impersonation?.originalId ?? user?.id ?? null,
              action: `ACCESS_DENIED_${error.code}`,
              entityType,
              entityId: entityId ? String(entityId) : undefined,
              detailsJson: {
                procedure: path,
                procedureName,
                type,
                errorCode: error.code,
                errorMessage: error.message,
                role: user?.role,
                input: sanitizedInput,
                ...(impersonation ? { impersonatedUserId: user?.id } : {}),
              } as Prisma.InputJsonValue,
              ipAddress: ctx.ip,
              userAgent: ctx.userAgent,
            },
          })
        } catch (auditError) {
          console.error('[ErrorAudit] Failed to log:', path, auditError)
        }
      }
    }

    // Always re-throw the original error
    throw error
  }
})

// =============================================================================
// Rate Limiting
// =============================================================================

/**
 * General rate limiter: 100 mutations per minute per user.
 * Applied to all authenticated mutation procedures.
 */
const withRateLimit = middleware(async ({ ctx, next, type }) => {
  // Only rate-limit mutations — queries are read-only
  if (type !== 'mutation') return next()

  const userId = ctx.session?.user?.id
  if (!userId) return next()

  const result = checkRateLimit(`trpc:${userId}`, 100, 60_000)
  if (!result.success) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: 'Too many requests. Please try again shortly.',
    })
  }

  return next()
})

/**
 * Strict rate limiter for AI-triggering procedures: 5 per hour per user.
 * Protects against runaway OpenAI API costs.
 */
const withAIRateLimit = middleware(async ({ ctx, next }) => {
  const userId = ctx.session?.user?.id
  if (!userId) return next()

  const result = checkRateLimit(`ai:${userId}`, 5, 60 * 60 * 1000)
  if (!result.success) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: 'AI operation rate limit exceeded. Maximum 5 per hour.',
    })
  }

  return next()
})

// =============================================================================
// Procedure Types
// =============================================================================

/**
 * Protected procedure - requires authenticated user.
 * Mutations rate-limited (100/min), auto-audited, errors tracked.
 */
export const protectedProcedure = t.procedure
  .use(isAuthenticated)
  .use(withRateLimit)
  .use(withErrorAudit)
  .use(withMutationAudit)

/**
 * Admin procedure - requires SUPER_ADMIN or PROGRAM_ADMIN role.
 * PROGRAM_ADMIN mutations are audit-logged; SUPER_ADMIN mutations skipped.
 * Errors tracked for all users.
 */
export const adminProcedure = t.procedure
  .use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN'))
  .use(withRateLimit)
  .use(withErrorAudit)
  .use(withMutationAudit)

/**
 * Super admin procedure - requires SUPER_ADMIN role.
 * No automatic mutation audit. Errors still tracked.
 */
export const superAdminProcedure = t.procedure
  .use(hasRole('SUPER_ADMIN'))
  .use(withErrorAudit)

/**
 * Jury procedure - requires JURY_MEMBER role.
 * All mutations auto-audited, errors tracked.
 */
export const juryProcedure = t.procedure
  .use(hasRole('JURY_MEMBER'))
  .use(withRateLimit)
  .use(withErrorAudit)
  .use(withMutationAudit)

/**
 * Mentor procedure - requires MENTOR role (or admin).
 * MENTOR and PROGRAM_ADMIN mutations are audit-logged, errors tracked.
 */
export const mentorProcedure = t.procedure
  .use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'MENTOR'))
  .use(withRateLimit)
  .use(withErrorAudit)
  .use(withMutationAudit)

/**
 * Observer procedure - requires OBSERVER role (read-only access).
 * Mutations (if any) audit-logged, errors tracked.
 */
export const observerProcedure = t.procedure
  .use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'))
  .use(withRateLimit)
  .use(withErrorAudit)
  .use(withMutationAudit)

/**
 * Audience procedure - requires any authenticated user.
 * All mutations auto-audited, errors tracked.
 */
export const audienceProcedure = t.procedure
  .use(isAuthenticated)
  .use(withRateLimit)
  .use(withErrorAudit)
  .use(withMutationAudit)

/**
 * AI rate limit middleware - apply to individual AI-triggering procedures.
 * 5 operations per hour per user to protect OpenAI API costs.
 */
export { withAIRateLimit }
