import { difference } from 'lodash'
import { useRouter } from 'next/router'

import { STORAGE_CLIENT_LIBRARY_MAPPINGS } from './Storage.constants'
import type { StoragePolicyFormField } from './Storage.types'
import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types'
import { convertKVStringArrayToJson } from '@/components/interfaces/Integrations/Wrappers/Wrappers.utils'
import { FDW } from '@/data/fdw/fdws-query'
import { Bucket } from '@/data/storage/buckets-query'
import { getDecryptedValues } from '@/data/vault/vault-secret-decrypted-value-query'
import { createWrappedSymbol } from '@/lib/helpers'

const shortHash = (str: string) => {
  let hash = 0
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i)
    hash = (hash << 5) - hash + char
    hash &= hash // Convert to 32bit integer
  }
  return new Uint32Array([hash])[0].toString(36)
}

export type PoliciesByBucket = { name: string | Symbol; policies: Policy[] }[]

/**
 * Formats the policies from the objects table in the storage schema
 * to be consumable for the storage policies dashboard.
 *
 * @param policies All policies from a table in a schema
 */
export const formatPoliciesForStorage = (
  buckets: Bucket[],
  policies: Policy[]
): PoliciesByBucket => {
  if (policies.length === 0) return []

  /**
   * Format policies from storage objects to:
   *  - Include bucket name
   *  - Strip away ${bucketName}_{idx} suffix
   *  - Strip away bucket_id from definitions
   *  Note, if the policy definition has no bucket_id, we skip the formatting
   */
  const formattedPolicies = formatStoragePolicies(buckets, policies)

  const policiesByBucket = groupPoliciesByBucket(formattedPolicies)
  return policiesByBucket
}

/**
 * Policy that belongs to a bucket which is not loaded yet (might not have been
 * paginated to yet, or might have been deleted)
 */
export const UNKNOWN_BUCKET_SYMBOL = createWrappedSymbol('unknown-bucket', 'Unknown')
/**
 * Policy that is not associated with a specific bucket
 */
export const UNGROUPED_POLICY_SYMBOL = createWrappedSymbol('ungrouped-policy', 'Ungrouped')

const formatStoragePolicies = (buckets: Bucket[], policies: Policy[]) => {
  const availableBuckets = buckets.map((bucket) => bucket.name)
  const formattedPolicies = policies.map((policy) => {
    const { definition: policyDefinition, check: policyCheck } = policy

    const bucketName =
      policyDefinition !== null
        ? extractBucketNameFromDefinition(policyDefinition)
        : extractBucketNameFromDefinition(policyCheck)

    if (bucketName) {
      const isBucketLoaded = availableBuckets.includes(bucketName)

      return {
        ...policy,
        bucket: isBucketLoaded ? bucketName : UNKNOWN_BUCKET_SYMBOL,
      }
    }

    return { ...policy, bucket: UNGROUPED_POLICY_SYMBOL }
  })

  return formattedPolicies
}

export const extractBucketNameFromDefinition = (definition: string | null) => {
  if (!definition) return null

  const definitionSegments = definition?.split(' AND ') ?? []
  const [bucketDefinition] = definitionSegments.filter((segment: string) =>
    segment.includes('bucket_id')
  )
  return bucketDefinition ? bucketDefinition.split("'")[1] : null
}

const groupPoliciesByBucket = (policies: (Policy & { bucket: string | Symbol })[]) => {
  const policiesByBucket = new Map<string | Symbol, Policy[]>()
  policies.forEach((policy) => {
    if (!policiesByBucket.has(policy.bucket)) {
      policiesByBucket.set(policy.bucket, [])
    }
    policiesByBucket.get(policy.bucket)?.push(policy)
  })
  return Array.from(policiesByBucket).map(([bucketName, policies]) => ({
    name: bucketName,
    policies,
  }))
}

export const createPayloadsForAddPolicy = (
  bucketName = '',
  policyFormFields: StoragePolicyFormField,
  addSuffixToPolicyName = true
) => {
  const { name: policyName, definition, allowedOperations, roles } = policyFormFields
  const formattedDefinition = definition ? definition.replace(/\s+/g, ' ').trim() : ''

  return allowedOperations.map((operation: any, idx: number) => {
    return createPayloadForNewPolicy(
      idx,
      bucketName,
      policyName,
      formattedDefinition,
      operation,
      roles,
      addSuffixToPolicyName
    )
  })
}

const createPayloadForNewPolicy = (
  idx: number,
  bucketName: string,
  policyName: string,
  definition: string,
  operation: string,
  roles: string[],
  addSuffixToPolicyName: boolean
) => {
  const hashedBucketName = shortHash(bucketName)
  return {
    name: addSuffixToPolicyName ? `${policyName} ${hashedBucketName}_${idx}` : policyName,
    definition: operation === 'INSERT' ? undefined : `(${definition})`,
    action: 'PERMISSIVE',
    check: operation === 'INSERT' ? `(${definition})` : undefined,
    command: operation,
    schema: 'storage',
    table: 'objects',
    roles: roles.length > 0 ? roles : undefined,
  }
}

// Used in the policy editor to highlight which library methods are allowed depending on which operations are allowed
export const deriveAllowedClientLibraryMethods = (allowedOperations = []) => {
  return Object.keys(STORAGE_CLIENT_LIBRARY_MAPPINGS).filter((method) => {
    const requiredOperations = (STORAGE_CLIENT_LIBRARY_MAPPINGS as any)[method]
    if (difference(requiredOperations, allowedOperations).length === 0) {
      return method
    }
  })
}

// Create policy SQL statements on save based on configuration.
// Used purely for previewing in the review step, not actually fired
const createSQLStatementForCreatePolicy = (
  idx: number,
  bucketName: string,
  policyName: string,
  definition: string,
  operation: string,
  selectedRoles: string[],
  addSuffixToPolicyName: boolean
) => {
  const hashedBucketName = shortHash(bucketName)
  const formattedPolicyName = addSuffixToPolicyName
    ? `${policyName} ${hashedBucketName}_${idx}`
    : policyName
  const description = `Add policy for the ${operation} operation under the policy "${policyName}"`
  const roles = selectedRoles.length === 0 ? ['public'] : selectedRoles

  const statement = `
    CREATE POLICY "${formattedPolicyName}"
    ON storage.objects
    FOR ${operation}
    TO ${roles.join(', ')}
    ${operation === 'INSERT' ? 'WITH CHECK' : 'USING'} (${definition});
`
    .replace(/\s+/g, ' ')
    .trim()
  return { description, statement }
}

export const createSQLPolicies = (
  bucketName: string,
  policyFormFields: StoragePolicyFormField,
  addSuffixToPolicyName = true
) => {
  const { name: policyName, definition, allowedOperations, roles } = policyFormFields
  const policies = allowedOperations.map((operation: any, idx: number) =>
    createSQLStatementForCreatePolicy(
      idx,
      bucketName,
      policyName,
      definition || '',
      operation,
      roles,
      addSuffixToPolicyName
    )
  )
  return policies
}

export const applyBucketIdToTemplateDefinition = (definition: string, bucketId: any) => {
  return definition.replace('{bucket_id}', `'${bucketId}'`)
}

export const useStorageV2Page = () => {
  const router = useRouter()
  return router.pathname.split('/')[4] as undefined | 'files' | 'analytics' | 'vectors' | 's3'
}

export const getDecryptedParameters = async ({
  ref,
  connectionString,
  wrapper,
  wrapperMeta,
}: {
  ref?: string
  connectionString?: string
  wrapper: FDW
  wrapperMeta: WrapperMeta
}) => {
  const wrapperServerOptions = wrapperMeta.server.options

  const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])

  const paramsToBeDecrypted = Object.fromEntries(
    new Map(
      Object.entries(serverOptions).filter(([key, _value]) => {
        return wrapperServerOptions.find((option) => option.name === key)?.encrypted
      })
    )
  )

  const decryptedValues = await getDecryptedValues({
    projectRef: ref,
    connectionString: connectionString,
    ids: Object.values(paramsToBeDecrypted),
  })

  const paramsWithDecryptedValues = Object.fromEntries(
    new Map(
      Object.entries(paramsToBeDecrypted).map(([name, id]) => {
        const decryptedValue = decryptedValues[id]
        return [name, decryptedValue]
      })
    )
  )

  return {
    ...serverOptions,
    ...paramsWithDecryptedValues,
  }
}
