import {
  ADD_PIECES_TYPEHASH,
  CREATE_DATA_SET_TYPEHASH,
  type ProviderInfo,
  RPC_URLS,
  type StorageContext,
  type StorageContextCallbacks,
  type StorageServiceOptions,
  Synapse,
  type SynapseOptions,
  type TelemetryConfig,
} from '@filoz/synapse-sdk'
import { type Provider as EthersProvider, JsonRpcProvider, type Signer, Wallet, WebSocketProvider } from 'ethers'
import type { Logger } from 'pino'
import { ADDRESS_ONLY_SIGNER_SYMBOL, AddressOnlySigner } from './address-only-signer.js'
import { DEFAULT_DATA_SET_METADATA, DEFAULT_STORAGE_CONTEXT_CONFIG } from './constants.js'
import { getTelemetryConfig } from './telemetry-config.js'

export * from './constants.js'

const WEBSOCKET_REGEX = /^ws(s)?:\/\//i

let synapseInstance: Synapse | null = null
let storageInstance: StorageContext | null = null
let currentProviderInfo: ProviderInfo | null = null
let activeProvider: any = null // Track the provider for cleanup

/**
 * Complete application configuration interface
 * This is the main config interface that can be imported by CLI and other consumers
 */
export interface Config {
  port: number
  host: string
  privateKey: string | undefined
  rpcUrl: string
  databasePath: string
  // TODO: remove this from core?
  carStoragePath: string
  logLevel: string
  warmStorageAddress: string | undefined
}

/**
 * Common options for all Synapse configurations
 */
interface BaseSynapseConfig extends Omit<SynapseOptions, 'withCDN' | 'warmStorageAddress' | 'telemetry'> {
  /** RPC endpoint for the target Filecoin network. Defaults to calibration. */
  rpcUrl?: string | undefined
  /** Optional override for WarmStorage contract address */
  warmStorageAddress?: string | undefined
  withCDN?: boolean | undefined
  /** Default metadata to apply when creating or reusing datasets */
  dataSetMetadata?: Record<string, string>
  /**
   * Telemetry configuration.
   * Defaults to enabled unless explicitly disabled.
   * @example
   * {
   *   sentryInitOptions: {
   *     enabled: false, // if want to disable telemetry.
   *   },
   *   sentrySetTags: {
   *     appName: "${your-app-name}",
   *   },
   * }
   */
  telemetry?: TelemetryConfig
}

/**
 * Standard authentication with private key
 */
export interface PrivateKeyConfig extends BaseSynapseConfig {
  privateKey: string
}

/**
 * Session key authentication with wallet address and session key
 */
export interface SessionKeyConfig extends BaseSynapseConfig {
  walletAddress: string
  sessionKey: string
}

/**
 * Signer-based authentication with ethers Signer
 */
export interface SignerConfig extends BaseSynapseConfig {
  signer: Signer
  /** Target Filecoin network (required for signer mode to determine default RPC) */
  network: 'mainnet' | 'calibration'
}

/**
 * Configuration for Synapse initialization
 *
 * Supports three authentication modes:
 * 1. Standard: privateKey only
 * 2. Session Key: walletAddress + sessionKey
 * 3. Signer: ethers Signer instance
 */
export type SynapseSetupConfig = PrivateKeyConfig | SessionKeyConfig | SignerConfig

/**
 * Structured service object containing the fully initialized Synapse SDK and
 * its storage context
 */
export interface SynapseService {
  synapse: Synapse
  storage: StorageContext
  providerInfo: ProviderInfo
}

/**
 * Dataset selection options for multi-tenant scenarios.
 *
 * This is a curated subset of Synapse SDK options focused on the common
 * use cases for filecoin-pin.
 */
export interface DatasetOptions {
  /**
   * Create a new dataset even if one exists for this wallet.
   *
   * Set to `true` when you want each user to have their own dataset
   * despite sharing the same wallet (e.g., multi-tenant websites and org/enterprise services using the same wallet).
   *
   * @default false
   */
  createNew?: boolean

  /**
   * Connect to a specific dataset by ID.
   *
   * Use this to reconnect to a user's existing dataset after retrieving
   * the ID from localStorage or a database.
   *
   * Takes precedence over `createNew` if both are provided.
   */
  useExisting?: number

  /**
   * Custom metadata to attach to the dataset.
   *
   * Note: If `useExisting` is provided, metadata is ignored since you're
   * connecting to an existing dataset.
   */
  metadata?: Record<string, string>
}

/**
 * Options for creating a storage context.
 */
export interface CreateStorageContextOptions {
  /**
   * Dataset selection options.
   */
  dataset?: DatasetOptions

  /**
   * Progress callbacks for tracking creation.
   */
  callbacks?: StorageContextCallbacks

  /**
   * Override provider selection by address.
   * Takes precedence over providerId if both are specified.
   */
  providerAddress?: string

  /**
   * Override provider selection by ID.
   */
  providerId?: number

  /**
   * Optional logger instance for detailed operation tracking and progress callbacks.
   * If not provided, logging will be skipped.
   */
  logger?: Partial<Logger> | undefined
}

/**
 * Reset the service instances (for testing)
 */
export function resetSynapseService(): void {
  synapseInstance = null
  storageInstance = null
  currentProviderInfo = null
  activeProvider = null
}

/**
 * Check if Synapse is using session key authentication
 *
 * Session key authentication uses an AddressOnlySigner which cannot sign transactions.
 * Payment operations (deposits, allowances) must be done by the owner wallet separately.
 *
 * Uses a Symbol to reliably detect AddressOnlySigner even across module boundaries.
 *
 * @param synapse - Initialized Synapse instance
 * @returns true if using session key authentication, false otherwise
 */
export function isSessionKeyMode(synapse: Synapse): boolean {
  try {
    const client = synapse.getClient()

    // The client might be wrapped in a NonceManager, check the underlying signer
    let signerToCheck: any = client
    if ('signer' in client && client.signer) {
      signerToCheck = client.signer
    }

    // Check for the AddressOnlySigner symbol (most reliable)
    return ADDRESS_ONLY_SIGNER_SYMBOL in signerToCheck && signerToCheck[ADDRESS_ONLY_SIGNER_SYMBOL] === true
  } catch {
    return false
  }
}

/**
 * Type guards for authentication configuration
 */
function isPrivateKeyConfig(config: Partial<SynapseSetupConfig>): config is PrivateKeyConfig {
  return 'privateKey' in config && config.privateKey != null
}

function isSessionKeyConfig(config: Partial<SynapseSetupConfig>): config is SessionKeyConfig {
  return (
    'walletAddress' in config && 'sessionKey' in config && config.walletAddress != null && config.sessionKey != null
  )
}

function isSignerConfig(config: Partial<SynapseSetupConfig>): config is SignerConfig {
  return 'signer' in config && config.signer != null
}

/**
 * Validate authentication configuration
 */
function validateAuthConfig(config: Partial<SynapseSetupConfig>): 'standard' | 'session-key' | 'signer' {
  const hasPrivateKey = isPrivateKeyConfig(config)
  const hasSessionKey = isSessionKeyConfig(config)
  const hasSigner = isSignerConfig(config)

  const authCount = [hasPrivateKey, hasSessionKey, hasSigner].filter(Boolean).length

  if (authCount === 0) {
    throw new Error('Authentication required: provide either privateKey, walletAddress + sessionKey, or signer')
  }

  if (authCount > 1) {
    throw new Error('Conflicting authentication: provide only one of privateKey, walletAddress + sessionKey, or signer')
  }

  if (hasPrivateKey) return 'standard'
  if (hasSessionKey) return 'session-key'
  return 'signer'
}

/**
 * Create ethers provider for the given RPC URL
 */
function createProvider(rpcURL: string): EthersProvider {
  if (WEBSOCKET_REGEX.test(rpcURL)) {
    return new WebSocketProvider(rpcURL)
  }
  return new JsonRpcProvider(rpcURL)
}

/**
 * Setup and verify session key, throws if expired
 */
async function setupSessionKey(synapse: Synapse, sessionWallet: Wallet, logger: Logger): Promise<void> {
  const sessionKey = synapse.createSessionKey(sessionWallet)

  // Verify permissions - fail fast if expired or expiring soon
  const expiries = await sessionKey.fetchExpiries([CREATE_DATA_SET_TYPEHASH, ADD_PIECES_TYPEHASH])
  const now = Math.floor(Date.now() / 1000)
  const bufferTime = 30 * 60 // 30 minutes in seconds
  const minValidTime = now + bufferTime
  const createDataSetExpiry = Number(expiries[CREATE_DATA_SET_TYPEHASH])
  const addPiecesExpiry = Number(expiries[ADD_PIECES_TYPEHASH])

  // For CREATE_DATA_SET:
  // - 0 means no permission granted (OK - can still add to existing datasets)
  // - > 0 but < minValidTime means expired/expiring (ERROR)
  // - >= minValidTime means valid (OK)
  const hasCreateDataSetPermission = createDataSetExpiry > 0
  const isCreateDataSetPermissionUnavailable = hasCreateDataSetPermission && createDataSetExpiry < minValidTime

  // For ADD_PIECES:
  // - Must always have valid permission
  const isAddPiecesPermissionUnavailable = addPiecesExpiry <= minValidTime

  if (isCreateDataSetPermissionUnavailable) {
    throw new Error(
      `Session key expired or expiring soon (requires 30+ minutes validity). CreateDataSet: ${new Date(createDataSetExpiry * 1000).toISOString()}`
    )
  }

  if (isAddPiecesPermissionUnavailable) {
    throw new Error(
      `Session key expired or expiring soon (requires 30+ minutes validity). AddPieces: ${new Date(addPiecesExpiry * 1000).toISOString()}`
    )
  }

  if (!hasCreateDataSetPermission) {
    logger.info(
      { event: 'synapse.session_key.limited_permissions' },
      'Session key can only add pieces to existing datasets (no CREATE_DATA_SET permission)'
    )
  }

  logger.info(
    { event: 'synapse.session_key.verified', createExpiry: createDataSetExpiry, addExpiry: addPiecesExpiry },
    'Session key verified'
  )

  synapse.setSession(sessionKey)
  logger.info({ event: 'synapse.session_key.activated' }, 'Session key activated')
}

/**
 * Initialize the Synapse SDK without creating storage context
 *
 * Supports three authentication modes:
 * - Standard: privateKey only
 * - Session Key: walletAddress + sessionKey
 * - Signer: ethers Signer instance
 *
 * @param config - Application configuration with authentication credentials
 * @param logger - Logger instance for detailed operation tracking
 * @returns Initialized Synapse instance
 */
export async function initializeSynapse(config: Partial<SynapseSetupConfig>, logger: Logger): Promise<Synapse> {
  const { withCDN, warmStorageAddress, telemetry, ...restConfig } = config
  try {
    const authMode = validateAuthConfig(config)

    // Determine RPC URL based on auth mode
    let rpcURL: string
    if (isSignerConfig(config)) {
      rpcURL = config.rpcUrl ?? RPC_URLS[config.network].websocket
    } else {
      rpcURL = config.rpcUrl ?? RPC_URLS.calibration.websocket
    }

    logger.info({ event: 'synapse.init', authMode, rpcUrl: rpcURL }, 'Initializing Synapse SDK')

    const synapseOptions: SynapseOptions = {
      ...restConfig,
      rpcURL,
      withIpni: true, // Always filter for IPNI-enabled providers
    }
    if (withCDN) {
      synapseOptions.withCDN = true
    }
    if (warmStorageAddress) {
      synapseOptions.warmStorageAddress = warmStorageAddress
    }
    synapseOptions.telemetry = getTelemetryConfig(telemetry)

    let synapse: Synapse

    if (authMode === 'session-key') {
      // Session key mode - type guard ensures these are defined
      if (!isSessionKeyConfig(config)) {
        throw new Error('Internal error: session key mode but config type mismatch')
      }

      // Create provider and signers for session key mode
      const provider = createProvider(rpcURL)
      activeProvider = provider

      const ownerSigner = new AddressOnlySigner(config.walletAddress, provider)
      const sessionWallet = new Wallet(config.sessionKey, provider)

      // Initialize with owner signer, then activate session key
      synapse = await Synapse.create({
        ...synapseOptions,
        signer: ownerSigner,
      })
      await setupSessionKey(synapse, sessionWallet, logger)
    } else if (authMode === 'signer') {
      // Signer mode - type guard ensures signer is defined
      if (!isSignerConfig(config)) {
        throw new Error('Internal error: signer mode but config type mismatch')
      }

      synapse = await Synapse.create({ ...synapseOptions, signer: config.signer })
      activeProvider = synapse.getProvider()
    } else {
      // Private key mode - type guard ensures privateKey is defined
      if (!isPrivateKeyConfig(config)) {
        throw new Error('Internal error: private key mode but config type mismatch')
      }

      synapse = await Synapse.create({ ...synapseOptions, privateKey: config.privateKey })
      activeProvider = synapse.getProvider()
    }

    const network = synapse.getNetwork()
    logger.info({ event: 'synapse.init.success', network }, 'Synapse SDK initialized')

    synapseInstance = synapse
    return synapse
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error)
    logger.error({ event: 'synapse.init.failed', error: errorMessage }, 'Failed to initialize Synapse SDK')
    throw error
  }
}

/**
 * Create storage context for an initialized Synapse instance
 *
 * This creates a storage context with comprehensive callbacks for tracking
 * the data set creation and provider selection process. This is primarily
 * a wrapper around the Synapse SDK's storage context creation, adding logging
 * and progress callbacks for better observability.
 *
 * @param synapse - Initialized Synapse instance
 * @param logger - Logger instance for detailed operation tracking
 * @param options - Optional configuration for dataset selection and callbacks
 * @returns Storage context and provider information
 *
 * @example
 * ```typescript
 * // Create a new dataset (multi-user scenario)
 * const { storage } = await createStorageContext(synapse, {
 *   logger,
 *   dataset: { createNew: true }
 * })
 *
 * // Connect to existing dataset
 * const { storage } = await createStorageContext(synapse, {
 *   logger,
 *   dataset: { useExisting: 123 }
 * })
 *
 * // Default behavior (reuse wallet's dataset)
 * const { storage } = await createStorageContext(synapse, { logger })
 * ```
 */
export async function createStorageContext(
  synapse: Synapse,
  options?: CreateStorageContextOptions
): Promise<{ storage: StorageContext; providerInfo: ProviderInfo }> {
  const logger = options?.logger

  try {
    // Create storage context with comprehensive event tracking
    // The storage context manages the data set and provider interactions
    logger?.info?.({ event: 'synapse.storage.create' }, 'Creating storage context')

    // Convert our curated options to Synapse SDK options
    const sdkOptions: StorageServiceOptions = {
      ...DEFAULT_STORAGE_CONTEXT_CONFIG,
    }

    // Apply dataset options
    if (options?.dataset?.useExisting != null) {
      sdkOptions.dataSetId = options.dataset.useExisting
      logger?.info?.(
        { event: 'synapse.storage.dataset.existing', dataSetId: options.dataset.useExisting },
        'Connecting to existing dataset'
      )
    } else if (options?.dataset?.createNew === true) {
      // If explicitly creating a new dataset in session key mode, verify we have permission
      if (isSessionKeyMode(synapse)) {
        const signer = synapse.getSigner()
        const sessionKey = synapse.createSessionKey(signer)

        const expiries = await sessionKey.fetchExpiries([CREATE_DATA_SET_TYPEHASH])
        const createDataSetExpiry = Number(expiries[CREATE_DATA_SET_TYPEHASH])

        if (createDataSetExpiry === 0) {
          throw new Error(
            'Cannot create new dataset: Session key does not have CREATE_DATA_SET permission. ' +
              'Either use an existing dataset or obtain a session key with dataset creation rights.'
          )
        }
      }

      sdkOptions.forceCreateDataSet = true
      logger?.info?.({ event: 'synapse.storage.dataset.create_new' }, 'Forcing creation of new dataset')
    }

    // Merge metadata (dataset metadata takes precedence)
    sdkOptions.metadata = {
      ...DEFAULT_DATA_SET_METADATA,
      ...options?.dataset?.metadata,
    }

    /**
     * Callbacks provide visibility into the storage lifecycle
     * These are crucial for debugging and monitoring in production
     */
    const callbacks: StorageContextCallbacks = {
      onProviderSelected: (provider) => {
        currentProviderInfo = provider

        logger?.info?.(
          {
            event: 'synapse.storage.provider_selected',
            provider: {
              id: provider.id,
              serviceProvider: provider.serviceProvider,
              name: provider.name,
              serviceURL: provider.products?.PDP?.data?.serviceURL,
            },
          },
          'Selected storage provider'
        )

        options?.callbacks?.onProviderSelected?.(provider)
      },
      onDataSetResolved: (info) => {
        logger?.info?.(
          {
            event: 'synapse.storage.data_set_resolved',
            dataSetId: info.dataSetId,
            isExisting: info.isExisting,
          },
          info.isExisting ? 'Using existing data set' : 'Created new data set'
        )

        options?.callbacks?.onDataSetResolved?.(info)
      },
    }

    sdkOptions.callbacks = callbacks

    // Apply provider override if present
    if (options?.providerAddress) {
      sdkOptions.providerAddress = options.providerAddress
      logger?.info?.(
        { event: 'synapse.storage.provider_override', providerAddress: options.providerAddress },
        'Overriding provider by address'
      )
    } else if (options?.providerId != null && Number.isFinite(options.providerId)) {
      sdkOptions.providerId = options.providerId
      logger?.info?.(
        { event: 'synapse.storage.provider_override', providerId: options.providerId },
        'Overriding provider by ID'
      )
    }

    const storage = await synapse.storage.createContext(sdkOptions)

    logger?.info?.(
      {
        event: 'synapse.storage.created',
        dataSetId: storage.dataSetId,
        serviceProvider: storage.serviceProvider,
      },
      'Storage context created successfully'
    )

    // Store instance
    storageInstance = storage

    // Ensure we always have provider info
    if (!currentProviderInfo) {
      // This should not happen as provider is selected during context creation
      throw new Error('Provider information not available after storage context creation')
    }

    return { storage, providerInfo: currentProviderInfo }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error)
    logger?.error?.(
      {
        event: 'synapse.storage.create.failed',
        error: errorMessage,
      },
      `Failed to create storage context: ${errorMessage}`
    )
    throw error
  }
}

/**
 * Set up complete Synapse service with SDK and storage context
 *
 * This function demonstrates the complete setup flow for Synapse:
 * 1. Validates required configuration (private key)
 * 2. Creates Synapse instance with network configuration
 * 3. Creates a storage context with comprehensive callbacks
 * 4. Returns a service object for application use
 *
 * Our wrapping of Synapse initialization and storage context creation is
 * primarily to handle our custom configuration needs and add detailed logging
 * and progress tracking.
 *
 * @param config - Application configuration with privateKey and RPC URL
 * @param logger - Logger instance for detailed operation tracking
 * @param options - Optional dataset selection and callbacks
 * @returns SynapseService with initialized Synapse and storage context
 *
 * @example
 * ```typescript
 * // Standard setup (reuses wallet's dataset)
 * const service = await setupSynapse(config, logger)
 *
 * // Create new dataset for multi-user scenario
 * const service = await setupSynapse(config, logger, {
 *   dataset: { createNew: true }
 * })
 *
 * // Connect to specific dataset
 * const service = await setupSynapse(config, logger, {
 *   dataset: { useExisting: 123 }
 * })
 * ```
 */
export async function setupSynapse(
  config: SynapseSetupConfig,
  logger: Logger,
  options?: CreateStorageContextOptions
): Promise<SynapseService> {
  // Initialize SDK
  const synapse = await initializeSynapse(config, logger)

  // Create storage context
  let storageOptions = options ? { ...options } : undefined
  if (config.dataSetMetadata && Object.keys(config.dataSetMetadata).length > 0) {
    storageOptions = {
      ...(storageOptions ?? {}),
      dataset: {
        ...(storageOptions?.dataset ?? {}),
        metadata: {
          ...config.dataSetMetadata,
          ...(storageOptions?.dataset?.metadata ?? {}),
        },
      },
    }
  }

  const { storage, providerInfo } = await createStorageContext(synapse, {
    ...(storageOptions ?? {}),
    logger,
  })

  return { synapse, storage, providerInfo }
}

/**
 * Get default storage context configuration for consistent data set creation
 *
 * @param overrides - Optional overrides to merge with defaults
 * @returns Storage context configuration with defaults
 */
export function getDefaultStorageContextConfig(overrides: any = {}) {
  return {
    ...DEFAULT_STORAGE_CONTEXT_CONFIG,
    ...overrides,
    metadata: {
      ...DEFAULT_DATA_SET_METADATA,
      ...overrides.metadata,
    },
  }
}

/**
 * Clean up a WebSocket provider connection
 * This is important for allowing the Node.js process to exit cleanly
 *
 * @param provider - The provider to clean up
 */
export async function cleanupProvider(provider: any): Promise<void> {
  if (provider && typeof provider.destroy === 'function') {
    // Suppress all errors during cleanup
    // WebSocket providers can throw async errors from scheduled operations
    // (like eth_unsubscribe) after destroy() is called
    const errorHandler = () => {
      // Silently ignore all cleanup errors
    }

    // Add error listener to suppress errors from async operations
    if (typeof provider.on === 'function') {
      provider.on('error', errorHandler)
    }

    try {
      await provider.destroy()
    } catch {
      // Ignore cleanup errors
    }

    // Small delay to allow any pending async operations to complete
    // This prevents errors from scheduled operations that trigger after destroy()
    await new Promise((resolve) => setTimeout(resolve, 100))
  }
}

/**
 * Clean up WebSocket providers and other resources
 *
 * Call this when CLI commands are finishing to ensure proper cleanup
 * and allow the process to terminate
 */
export async function cleanupSynapseService(): Promise<void> {
  // Close telemetry to flush pending events and shutdown cleanly
  if (synapseInstance) {
    await synapseInstance.telemetry?.sentry?.close()
  }

  if (activeProvider) {
    await cleanupProvider(activeProvider)
  }

  // Clear references
  synapseInstance = null
  storageInstance = null
  currentProviderInfo = null
  activeProvider = null
}

/**
 * Get the initialized Synapse service
 */
export function getSynapseService(): SynapseService | null {
  if (synapseInstance == null || storageInstance == null || currentProviderInfo == null) {
    return null
  }

  return {
    synapse: synapseInstance,
    storage: storageInstance,
    providerInfo: currentProviderInfo,
  }
}
