/**
 * This file is here only to show you how to proceed if you would
 * like to run your application as a standalone executable.
 *
 * You can launch it with the command `npm run standalone`
 */

import Fastify from 'fastify'
import fp from 'fastify-plugin'
import {createClient, RedisClientType} from 'redis'

// Import library to exit fastify process, gracefully (if possible)
import closeWithGrace from 'close-with-grace'

// Import your application as a normal plugin.
import serviceApp from './app.js'

// Idempotency storage using Redis
interface IdempotencyEntry {
  requestId: string
  response: any
  statusCode: number
  timestamp: number
  ttl: number // Time to live in milliseconds
}

let redisClient: RedisClientType | null = null
const IDEMPOTENCY_TTL = 5 * 60 // 5 minutes TTL in seconds (Redis uses seconds)

// Initialize Redis connection
async function initializeRedis() {
  try {
    const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'
    const redisPassword = process.env.REDIS_PASSWORD

    console.log('Redis URL:', redisUrl)
    console.log('Redis Password:', redisPassword ? '***' : 'none')
    
    // Create Redis client with password support and timeouts
    const redisConfig: any = { 
      url: redisUrl,
      socket: {
        connectTimeout: 5000, // 5 second connection timeout
        reconnectStrategy: (retries: number) => {
          // Exponential backoff: 50ms, 100ms, 200ms, 400ms, etc., max 3000ms
          if (retries > 10) {
            console.error('Redis: Max reconnection attempts reached')
            return new Error('Max reconnection attempts reached');
          }
          return Math.min(retries * 50, 3000);
        }
      }
    }
    
    if (redisPassword) {
      redisConfig.password = redisPassword
    }
    
    redisClient = createClient(redisConfig)
    
    redisClient.on('error', (err) => {
      console.error('Redis Client Error:', err)
    })
    
    redisClient.on('connect', () => {
      console.log('Connected to Redis for idempotency')
    })
    
    redisClient.on('ready', () => {
      console.log('Redis client ready for idempotency operations')
    })
    
    redisClient.on('reconnecting', () => {
      console.log('Redis client reconnecting...')
    })
    
    // Connect with timeout
    await Promise.race([
      redisClient.connect(),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error('Redis connection timeout after 5s')), 5000)
      )
    ])
  } catch (error) {
    console.error('Failed to connect to Redis:', error)
    // Fallback to in-memory storage if Redis is not available
    console.warn('Falling back to in-memory idempotency storage')
  }
}

// Fallback in-memory storage for when Redis is not available
const fallbackStore = new Map<string, IdempotencyEntry>()

// Helper function to get idempotency entry
async function getIdempotencyEntry(requestId: string): Promise<IdempotencyEntry | null> {
  if (redisClient && redisClient.isOpen) {
    try {
      const data = await redisClient.get(`idempotency:${requestId}`)
      if (data && typeof data === 'string') {
        return JSON.parse(data)
      }
      return null
    } catch (error) {
      console.error('Redis get error:', error)
      return null
    }
  } else {
    // Fallback to in-memory storage
    return fallbackStore.get(requestId) || null
  }
}

// Helper function to set idempotency entry
async function setIdempotencyEntry(requestId: string, entry: IdempotencyEntry): Promise<void> {
  if (redisClient && redisClient.isOpen) {
    try {
      await redisClient.setEx(`idempotency:${requestId}`, IDEMPOTENCY_TTL, JSON.stringify(entry))
    } catch (error) {
      console.error('Redis set error:', error)
      // Fallback to in-memory storage
      fallbackStore.set(requestId, entry)
    }
  } else {
    // Fallback to in-memory storage
    fallbackStore.set(requestId, entry)
  }
}

/**
 * Do not use NODE_ENV to determine what logger (or any env related feature) to use
 * @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o}
 */
function getLoggerOptions () {
  // Only if the program is running in an interactive terminal
  if (process.stdout.isTTY) {
    return {
      level: 'info',
      transport: {
        target: 'pino-pretty',
        options: {
          translateTime: 'HH:MM:ss Z',
          ignore: 'pid,hostname'
        }
      }
    }
  }

  return { level: process.env.LOG_LEVEL ?? 'info' } // Changed from 'silent' to 'info' for better debugging
}

const app = Fastify({
  logger: getLoggerOptions(),
  ajv: {
    customOptions: {
      coerceTypes: 'array', // change type of data to match type keyword
      removeAdditional: 'all' // Remove additional body properties
    }
  },
  // Add connection and timeout settings for better resilience
  connectionTimeout: 30000, // 30 seconds
  keepAliveTimeout: 5000,   // 5 seconds
  bodyLimit: 1048576,       // 1MB
  maxParamLength: 200,      // 200 characters
  // Add request timeout
  requestTimeout: 30000,    // 30 seconds
})

async function init () {
  // Initialize Redis connection
  await initializeRedis()

  // Add idempotency pre-handler hook
  app.addHook('preHandler', async (request, reply) => {
    // Only apply idempotency to POST requests (trading operations)
    if (request.method !== 'POST') {
      return
    }

    const requestId = request.headers['idempotency-key'] || request.headers['x-request-id']
    
    if (!requestId) {
      // No idempotency key provided, continue normally
      return
    }

    // Check if we've seen this request before
    const existingEntry = await getIdempotencyEntry(requestId as string)
    
    if (existingEntry) {
      // Check if entry is still valid (Redis TTL handles this, but double-check for fallback)
      const now = Date.now()
      if (now - existingEntry.timestamp <= existingEntry.ttl) {
        app.log.info(`Idempotency: Returning cached response for request ${requestId}`)
        
        // Return the cached response
        reply.code(existingEntry.statusCode)
        return existingEntry.response
      } else {
        // Entry expired, remove it (only needed for fallback storage)
        if (!redisClient || !redisClient.isOpen) {
          fallbackStore.delete(requestId as string)
        }
      }
    }

    // Store the request ID for later use using a symbol to avoid TypeScript property errors
    const IDEMPOTENCY_KEY_SYMBOL = Symbol.for('idempotency-key')
    // Attach the symbol to the request object
    ;(request as any)[IDEMPOTENCY_KEY_SYMBOL] = requestId as string

  })

  // Add post-handler hook to store successful responses
  app.addHook('onSend', async (request, reply, payload) => {
    // Retrieve the idempotency key from the symbol
    const IDEMPOTENCY_KEY_SYMBOL = Symbol.for('idempotency-key')
    const idempotencyKey = (request as any)[IDEMPOTENCY_KEY_SYMBOL]
    if (idempotencyKey && request.method === 'POST') {
      const requestId = idempotencyKey
      
      // Only store successful responses (2xx status codes)
      if (reply.statusCode >= 200 && reply.statusCode < 300) {
        try {
          const responseData = typeof payload === 'string' ? JSON.parse(payload) : payload
          
          const entry: IdempotencyEntry = {
            requestId,
            response: responseData,
            statusCode: reply.statusCode,
            timestamp: Date.now(),
            ttl: IDEMPOTENCY_TTL * 1000 // Convert to milliseconds for consistency
          }
          
          await setIdempotencyEntry(requestId, entry)
          
          app.log.info(`Idempotency: Stored response for request ${requestId}`)
        } catch (error) {
          app.log.error(error, `Idempotency: Failed to store response for request ${requestId}`)
        }
      }
    }
  })

  // Register your application as a normal plugin.
  // fp must be used to override default error handler
  app.register(fp(serviceApp))

  // Delay is the number of milliseconds for the graceful close to finish
  closeWithGrace(
    { delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 5000 }, // Increased from 500ms to 5s
    async ({ err }) => {
      if (err != null) {
        app.log.error(err)
      }

      // Close Redis connection gracefully
      if (redisClient && redisClient.isOpen) {
        try {
          await redisClient.quit()
          console.log('Redis connection closed gracefully')
        } catch (error) {
          console.error('Error closing Redis connection:', error)
        }
      }

      await app.close()
    }
  )

  await app.ready()

  try {
    // Start listening with better configuration
    await app.listen({ 
      port: 4111,
      host: '0.0.0.0', // Listen on all interfaces
      backlog: 511,    // Increase backlog for better connection handling
    })
    
    app.log.info('Web3Proxy server started successfully on port 4111')
  } catch (err) {
    app.log.error(err)
    process.exit(1)
  }
}

init()
