import invariant from 'tiny-invariant'
import {
  http,
  type Address,
  type Hex,
  type PublicClient,
  createPublicClient,
} from 'viem'
import { arbitrumSepolia } from 'viem/chains'
import { persist, subscribeWithSelector } from 'zustand/middleware'
import { type Mutate, type StoreApi, createStore } from 'zustand/vanilla'
import type { SupportedChainId } from './chains/defaults.js'
import type { ExternalConfig } from './createExternalKeyConfig.js'
import { type Storage, createStorage, noopStorage } from './createStorage.js'
import type { Evaluate, ExactPartial } from './types/utils.js'
import type * as rustUtils from './utils.d.ts'
import { AuthType } from './utils/websocket.js'

export type CreateConfigParameters = {
  chainId: SupportedChainId
  darkPoolAddress: Address
  priceReporterUrl: string
  relayerUrl: string
  httpPort?: number
  pollingInterval?: number
  ssr?: boolean | undefined
  storage?: Storage | null | undefined
  useInsecureTransport?: boolean
  utils?: typeof rustUtils
  websocketPort?: number
  viemClient?: PublicClient
  adminKey?: string
}

export function createConfig(
  parameters: CreateConfigParameters,
): InternalConfig {
  const {
    relayerUrl,
    priceReporterUrl,
    httpPort = 3000,
    pollingInterval = 5000,
    ssr,
    storage = createStorage({
      storage:
        typeof window !== 'undefined' && window.localStorage
          ? window.localStorage
          : noopStorage,
    }),
    useInsecureTransport = false,
    viemClient = createPublicClient({
      chain: arbitrumSepolia,
      transport: http(),
    }),
    websocketPort = 4000,
    adminKey,
  } = parameters

  invariant(
    parameters.utils,
    'Utils must be provided by the package if not supplied by the user.',
  )

  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Create store
  /////////////////////////////////////////////////////////////////////////////////////////////////

  function getInitialState(): State {
    return {
      seed: undefined,
      status: 'disconnected',
      id: undefined,
    }
  }

  const store = createStore(
    subscribeWithSelector(
      // only use persist middleware if storage exists
      storage
        ? persist(getInitialState, {
            name: 'store',
            partialize(state) {
              // Only persist "critical" store properties to preserve storage size.
              return {
                id: state.id,
                seed: state.seed,
                status: state.status,
              } satisfies PartializedState
            },
            skipHydration: ssr,
            storage: storage as Storage<Record<string, unknown>>,
          })
        : getInitialState,
    ),
  )

  return {
    utils: parameters.utils,
    renegadeKeyType: 'internal' as const,
    storage,
    relayerUrl,
    priceReporterUrl,
    darkPoolAddress: parameters.darkPoolAddress,
    getBaseUrl: (route = '') => {
      const protocol =
        useInsecureTransport || parameters.relayerUrl.includes('localhost')
          ? 'http'
          : 'https'
      const baseUrl = parameters.relayerUrl.includes('localhost')
        ? `127.0.0.1:${httpPort}/v0`
        : `${parameters.relayerUrl}:${httpPort}/v0`
      const formattedRoute = route.startsWith('/') ? route : `/${route}`
      return `${protocol}://${baseUrl}${formattedRoute}`
    },
    getPriceReporterBaseUrl: () => {
      const baseUrl = parameters.priceReporterUrl.includes('localhost')
        ? `ws://127.0.0.1:${websocketPort}/`
        : `wss://${parameters.priceReporterUrl}:${websocketPort}/`
      return baseUrl
    },
    getPriceReporterHTTPBaseUrl: (route = '') => {
      const baseUrl = parameters.priceReporterUrl.includes('localhost')
        ? `http://127.0.0.1:${httpPort}`
        : `https://${parameters.priceReporterUrl}:${httpPort}`
      const formattedRoute = route.startsWith('/') ? route : `/${route}`
      return `${baseUrl}${formattedRoute}`
    },
    getWebsocketBaseUrl: () => {
      const protocol =
        useInsecureTransport || parameters.relayerUrl.includes('localhost')
          ? 'ws'
          : 'wss'
      const baseUrl = parameters.relayerUrl.includes('localhost')
        ? `127.0.0.1:${websocketPort}`
        : `${parameters.relayerUrl}:${websocketPort}`
      return `${protocol}://${baseUrl}`
    },
    getSymmetricKey(type?: AuthType) {
      invariant(parameters.utils, 'Utils are required')
      if (type === AuthType.Admin) {
        invariant(parameters.adminKey, 'Admin key is required')
        const symmetricKey = parameters.utils.b64_to_hex_hmac_key(
          parameters.adminKey,
        ) as Hex
        invariant(symmetricKey, 'Admin key is required')
        return symmetricKey
      }
      const seed = store.getState().seed
      invariant(seed, 'Seed is required')
      return parameters.utils.get_symmetric_key(seed) as Hex
    },
    pollingInterval,
    get state() {
      return store.getState()
    },
    setState(value) {
      let newState: State
      if (typeof value === 'function') newState = value(store.getState() as any)
      else newState = value

      // Reset state if it got set to something not matching the base state
      const initialState = getInitialState()
      if (typeof newState !== 'object') newState = initialState
      const isCorrupt = Object.keys(initialState).some((x) => !(x in newState))
      if (isCorrupt) newState = initialState

      store.setState(newState, true)
    },
    subscribe(selector, listener, options) {
      return store.subscribe(
        selector as unknown as (state: State) => any,
        listener,
        options
          ? { ...options, fireImmediately: options.emitImmediately }
          : undefined,
      )
    },
    viemClient,
    adminKey,
    _internal: {
      store,
      ssr: Boolean(ssr),
    },
    chainId: parameters.chainId,
  }
}

export type BaseConfig = {
  utils: typeof rustUtils
  getWebsocketBaseUrl: () => string
  getBaseUrl: (route?: string) => string
  getSymmetricKey: (type?: AuthType) => Hex
}

export type Config = BaseConfig & {
  chainId: SupportedChainId
  renegadeKeyType: 'internal'
  readonly storage: Storage | null
  darkPoolAddress: Address
  getPriceReporterBaseUrl: () => string
  getPriceReporterHTTPBaseUrl: (route?: string) => string
  pollingInterval: number
  priceReporterUrl: string
  relayerUrl: string
  setState(value: State | ((state: State) => State)): void
  state: State
  subscribe<state>(
    selector: (state: State) => state,
    listener: (state: state, previousState: state) => void,
    options?:
      | {
          emitImmediately?: boolean | undefined
          equalityFn?: ((a: state, b: state) => boolean) | undefined
        }
      | undefined,
  ): () => void
  viemClient: PublicClient
  adminKey?: string
  /**
   * Not part of versioned API, proceed with caution.
   * @internal
   */
  _internal: {
    readonly store: Mutate<StoreApi<any>, [['zustand/persist', any]]>
    readonly ssr: boolean
  }
}

// For backwards-compatibility
export type InternalConfig = Config

export type RenegadeConfig = InternalConfig | ExternalConfig

export interface State {
  seed?: Hex | undefined
  status?:
    | 'in relayer'
    | 'disconnected'
    | 'looking up'
    | 'creating wallet'
    | 'connecting'
  id?: string | undefined
}

// The type of keychain a config is using
export const keyTypes = {
  EXTERNAL: 'external',
  INTERNAL: 'internal',
  NONE: 'none',
} as const

export type PartializedState = Evaluate<
  ExactPartial<Pick<State, 'id' | 'seed' | 'status'>>
>
