import { zodResolver } from '@hookform/resolvers/zod'
import { Box } from '@tidbcloud/uikit'
import { useMemoizedFn } from 'ahooks'
import * as pwdGenerator from 'generate-password'
import { observer } from 'mobx-react'
import { useEffect, useId, useMemo, useRef, useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { useHistory, useLocation, useParams } from 'react-router-dom'

import { RBACRoles } from 'dbaas/contexts/access-control/interface/rbac'
import { getSharedCluster } from 'dbaas/features/ClusterManagement/cluster'
import { getCodeVarData, getConnCode } from 'dbaas/screens/ClusterV2/features/Connection/serverless/helpers'
import { connCfg } from 'dbaas/screens/ClusterV2/features/Connection/serverless/meta'
import { Endpoint } from 'dbaas/screens/ClusterV2/features/Connection/serverless/types'
import { handle401Error } from 'dbaas/screens/OAuth/Authorize/helpers'
import { useGetAllSpendLimit } from 'dbaas/services'
import { useGetUserOrgsProjects } from 'dbaas/services/account'
import {
  serverlessServiceChangeRootPassword,
  useServerlessServiceGetCluster
} from 'dbaas/services/api-clients/v2023/serverless/v1beta1'
import { TidbCloudOpenApiserverlessv1beta1Cluster } from 'dbaas/services/api-clients/v2023/serverless/v1beta1/interface'
import { getErrorMessage } from 'dbaas/services/errorCodes'
import useStores from 'dbaas/stores/useStores'
import { eventTracking, MixpanelEvents, PRODUCT_TYPES, reportGA } from 'dbaas/utils/tracking'
import claimBg from 'media/claim_bg.svg'

import { ClaimActions } from './ClaimActions'
import { ClaimForm } from './ClaimForm'
import { ClaimProvisioning, ClaimProvisioningStage } from './ClaimProvisioning'
import { ClaimSummary } from './ClaimSummary'
import { getDefaultClaimInstanceName } from './get-default-instance-name'
import { claimFormSchema } from './schema'
import { ClaimFormValue, ClaimSelectOption } from './types'
import { useClaimStarter } from './use-claim-starter'

const CLAIM_CLUSTER_POLL_INTERVAL = 3 * 1000

interface ClaimClusterContext {
  clusterId: string
  orgId: string
  projectId: string
}

type ClaimSetupStage = 'idle' | ClaimProvisioningStage

const stripHtmlTags = (message: string) => message.replace(/<[^>]*>/g, '').trim()
const CLAIM_SETUP_ERROR_MSG = 'Failed to claim and initialize the Starter instance.'
const CLAIM_GA_SOURCE = 'zero_claim'

const normalizeSetupErrorMessage = (error: unknown) => {
  let rawMessage: unknown = ''

  try {
    rawMessage = getErrorMessage(error, true)
  } catch (_error) {
    return CLAIM_SETUP_ERROR_MSG
  }

  if (typeof rawMessage !== 'string' && rawMessage && typeof rawMessage === 'object' && 'message' in rawMessage) {
    rawMessage = (rawMessage as { message?: unknown }).message
  }

  const message = typeof rawMessage === 'string' ? rawMessage : String(rawMessage || '')
  if (message.includes('not claimable')) {
    return 'This database is no longer claimable — it may have expired or already been saved as a Starter instance.'
  }

  const cleanedMessage = stripHtmlTags(message)

  return cleanedMessage || CLAIM_SETUP_ERROR_MSG
}

const buildConnectionString = ({
  cluster,
  password
}: {
  cluster: TidbCloudOpenApiserverlessv1beta1Cluster
  password: string
}) => {
  const endpoint = cluster.endpoints?.public?.disabled ? Endpoint.Private : Endpoint.Public
  const endpointData =
    endpoint === Endpoint.Private
      ? {
          host: cluster.endpoints?.private?.host,
          port: cluster.endpoints?.private?.port
        }
      : {
          host: cluster.endpoints?.public?.host,
          port: cluster.endpoints?.public?.port
        }

  if (!endpointData.host || !endpointData.port || !cluster.userPrefix) {
    return ''
  }

  const template =
    endpoint === Endpoint.Private ? connCfg.privateGeneralstring?.code || '' : connCfg.publicGeneralstring?.code || ''
  const templateWithoutDb = template.replace(/\/\$\{database\}/, '')
  const codeVars = getCodeVarData({
    endpointData,
    userToken: cluster.userPrefix,
    pwd: password,
    os: 'other'
  })

  return getConnCode(templateWithoutDb, codeVars)
}

const getOverviewPath = ({
  tidbId,
  search,
  oneConsoleEnabled,
  orgId
}: {
  tidbId: string
  search: string
  oneConsoleEnabled?: boolean
  orgId?: string
}) => {
  const params = new URLSearchParams(search)
  if (orgId) {
    params.set('orgId', orgId)
  }

  const query = params.toString()
  const basePath = oneConsoleEnabled ? '/tidbs' : '/clusters'
  return `${basePath}/${tidbId}/overview${query ? `?${query}` : ''}`
}

const ClaimPage = observer(() => {
  const { id: claimId } = useParams<{ id: string }>()
  const history = useHistory()
  const location = useLocation()

  const { data, isLoading: isScopeLoading } = useGetUserOrgsProjects({
    request: {
      isHandleErr: true,
      _warningStatus: [400, 401]
    },
    query: {
      refetchInterval: false,
      retry: false,
      refetchOnWindowFocus: false,
      onError(err: any) {
        if (err?.response?.status === 401) {
          handle401Error()
          return
        }
      }
    }
  })

  const filteredProjects = useMemo(() => {
    const orgs = data?.orgs || []
    const projects = data?.projects || []
    const orgRoleById = orgs.reduce<Record<string, string>>((prev, org) => {
      if (org.id) {
        prev[org.id] = org.org_role || ''
      }
      return prev
    }, {})
    const isImpersonated = orgs.some((org) => org.org_role === RBACRoles.OrgImpersonated)

    return projects.filter((project) => {
      if (!project?.id || !project?.org_id) {
        return false
      }

      if (isImpersonated) {
        return true
      }

      const orgRole = orgRoleById[project.org_id]
      return orgRole === RBACRoles.OrgOwner || project.project_role === RBACRoles.ProjectOwner
    })
  }, [data?.orgs, data?.projects])
  const allowedOrgIds = useMemo(() => {
    return new Set(filteredProjects.map((project) => project.org_id).filter((id): id is string => !!id))
  }, [filteredProjects])

  const orgOptions = useMemo<ClaimSelectOption[]>(
    () =>
      (data?.orgs || [])
        .filter((org) => !!org?.id && allowedOrgIds.has(org.id))
        .filter((org): org is { id: string; name?: string } => !!org?.id)
        .map((org) => ({
          label: org.name || org.id,
          value: org.id
        })),
    [allowedOrgIds, data?.orgs]
  )

  const orgNameById = useMemo<Record<string, string>>(
    () =>
      (data?.orgs || []).reduce<Record<string, string>>((prev, org) => {
        if (!org.id || !allowedOrgIds.has(org.id)) {
          return prev
        }
        prev[org.id] = org.name || org.id
        return prev
      }, {}),
    [allowedOrgIds, data?.orgs]
  )

  const projectOptionsByOrg = useMemo<Record<string, ClaimSelectOption[]>>(
    () =>
      filteredProjects.reduce<Record<string, ClaimSelectOption[]>>((prev, project) => {
        if (!project.id || !project.org_id) {
          return prev
        }
        const option = {
          label: project.name || project.id,
          value: project.id
        }
        if (prev[project.org_id]) {
          prev[project.org_id].push(option)
        } else {
          prev[project.org_id] = [option]
        }
        return prev
      }, {}),
    [filteredProjects]
  )

  const projectNameById = useMemo<Record<string, string>>(
    () =>
      filteredProjects.reduce<Record<string, string>>((prev, project) => {
        if (!project.id) {
          return prev
        }
        prev[project.id] = project.name || project.id
        return prev
      }, {}),
    [filteredProjects]
  )

  const {
    store: {
      config: { oneConsoleEnabled }
    },
    actions: { refreshOrgData, updateLastVisitLocation }
  } = useStores()
  const formId = useId()
  const mountedRef = useRef(true)
  const readySetupInFlightRef = useRef(false)
  const [setupStage, setSetupStage] = useState<ClaimSetupStage>('idle')
  const [setupErrorMessage, setSetupErrorMessage] = useState('')
  const [setupConnectionString, setSetupConnectionString] = useState('')
  const [setupConsoleContext, setSetupConsoleContext] = useState<ClaimClusterContext | null>(null)
  const [pollingContext, setPollingContext] = useState<ClaimClusterContext | null>(null)

  useEffect(
    () => () => {
      mountedRef.current = false
    },
    []
  )

  const formMethods = useForm<ClaimFormValue>({
    mode: 'onChange',
    defaultValues: {
      orgId: '',
      projectId: '',
      provider: '',
      region: '',
      name: getDefaultClaimInstanceName(claimId)
    },
    resolver: zodResolver(claimFormSchema)
  })
  const selectedOrgId = formMethods.watch('orgId')
  const selectedProjectId = formMethods.watch('projectId')
  const syncLastVisitLocation = useMemoizedFn((orgId: string, projectId: string) => {
    if (!orgId || !projectId) {
      return
    }

    const projectBelongsToOrg = projectOptionsByOrg[orgId]?.some((project) => project.value === projectId)
    if (!projectBelongsToOrg) {
      return
    }

    updateLastVisitLocation({
      orgId,
      projectId
    })
  })
  const handleOrgChange = useMemoizedFn((orgId: string) => {
    const projectOptions = projectOptionsByOrg[orgId] || []
    const projectId = projectOptions.some((project) => project.value === selectedProjectId)
      ? selectedProjectId
      : projectOptions[0]?.value || ''

    syncLastVisitLocation(orgId, projectId)
  })
  const handleProjectChange = useMemoizedFn((projectId: string) => {
    syncLastVisitLocation(selectedOrgId, projectId)
  })
  const { data: spendLimitData, isLoading: isSpendLimitLoading } = useGetAllSpendLimit(
    selectedOrgId,
    { project_id: selectedProjectId },
    {
      query: {
        enabled: !!(selectedOrgId && selectedProjectId),
        refetchOnWindowFocus: false
      }
    }
  )
  const freeQuotaExhausted = spendLimitData === undefined ? false : !spendLimitData.has_free_credit_quota
  const isClaimContextLoading = isScopeLoading || (!!selectedOrgId && !!selectedProjectId && isSpendLimitLoading)

  const { claimTidb } = useClaimStarter({
    claimId,
    freeQuotaExhausted
  })

  const finishSetupWithError = useMemoizedFn((error: unknown) => {
    const errorMessage = normalizeSetupErrorMessage(error)

    reportGA({
      event: 'zero_claim_failed',
      zero_id: claimId,
      source: CLAIM_GA_SOURCE,
      error_message: errorMessage,
      product_type: PRODUCT_TYPES.SERVERLESS
    })
    eventTracking(MixpanelEvents.ZeroClaim.Create.RequestFailed, {
      error: errorMessage
    })
    readySetupInFlightRef.current = false
    setSetupStage('failed')
    setSetupErrorMessage(errorMessage)
    setSetupConnectionString('')
    setSetupConsoleContext(null)
    setPollingContext(null)
  })

  const runReadySetup = useMemoizedFn(
    async (serverlessCluster: TidbCloudOpenApiserverlessv1beta1Cluster, context: ClaimClusterContext) => {
      if (readySetupInFlightRef.current) {
        return
      }
      readySetupInFlightRef.current = true
      setSetupStage('resetting')

      try {
        const password = pwdGenerator.generate({
          length: 16,
          numbers: true,
          strict: true
        })
        await serverlessServiceChangeRootPassword(
          context.clusterId,
          {
            password
          },
          {
            orgId: context.orgId,
            projectId: context.projectId
          }
        )

        if (!mountedRef.current) {
          return
        }

        const connectionString = buildConnectionString({
          cluster: serverlessCluster,
          password
        })
        if (!connectionString) {
          throw new Error('Failed to build connection string from current cluster endpoint data.')
        }

        setSetupConnectionString(connectionString)
        setSetupConsoleContext(context)
        setSetupStage('done')
        setPollingContext(null)
        reportGA({
          event: 'zero_claim_succeeded',
          zero_id: claimId,
          cluster_id: context.clusterId,
          org_id: context.orgId,
          project_id: context.projectId,
          source: CLAIM_GA_SOURCE,
          product_type: PRODUCT_TYPES.SERVERLESS
        })
        eventTracking(MixpanelEvents.ZeroClaim.Create.RequestSucceeded)
      } catch (error) {
        if (!mountedRef.current) {
          return
        }
        finishSetupWithError(error)
      }
    }
  )

  const pollingClusterId = pollingContext?.clusterId || ''
  useServerlessServiceGetCluster(
    pollingClusterId,
    {
      orgId: pollingContext?.orgId || '',
      projectId: pollingContext?.projectId || '',
      view: 'BASIC'
    },
    {
      query: {
        enabled:
          setupStage === 'polling' &&
          !!pollingContext?.clusterId &&
          !!pollingContext?.orgId &&
          !!pollingContext?.projectId,
        refetchOnWindowFocus: false,
        refetchInterval: (data) => {
          if (!data) {
            return CLAIM_CLUSTER_POLL_INTERVAL
          }

          const sharedCluster = getSharedCluster(data)
          return sharedCluster?.computed.ready ? false : CLAIM_CLUSTER_POLL_INTERVAL
        },
        onSuccess: (serverlessCluster) => {
          if (setupStage !== 'polling' || !pollingContext) {
            return
          }

          const sharedCluster = getSharedCluster(serverlessCluster)
          if (!sharedCluster?.computed.ready) {
            return
          }

          void runReadySetup(serverlessCluster, pollingContext)
        },
        onError: (error) => {
          if (setupStage !== 'polling' || !pollingContext) {
            return
          }
          finishSetupWithError(error)
        }
      }
    }
  )

  const isClaimFlowRunning = setupStage === 'claiming' || setupStage === 'polling' || setupStage === 'resetting'
  const shouldRenderProvisioning = setupStage !== 'idle'

  const renderProvisioningStage = useMemo<ClaimProvisioningStage>(() => {
    if (setupStage === 'idle') {
      return 'claiming'
    }
    return setupStage
  }, [setupStage])

  const handleOpenConsole = useMemoizedFn(() => {
    refreshOrgData()
    if (setupConsoleContext) {
      const projectId = setupConsoleContext.projectId
      const tidbId = setupConsoleContext?.clusterId
      const orgId = setupConsoleContext?.orgId

      updateLastVisitLocation({
        orgId,
        projectId
      })

      if (tidbId) {
        history.push(
          getOverviewPath({
            tidbId,
            search: location.search,
            oneConsoleEnabled,
            orgId
          })
        )
      }
    } else {
      history.push(`/`)
    }
  })

  const handleValidSubmit = useMemoizedFn(async (formData: ClaimFormValue) => {
    reportGA({
      event: 'zero_claim_started',
      zero_id: claimId,
      org_id: formData.orgId,
      project_id: formData.projectId,
      provider: formData.provider,
      region: formData.region,
      free_quota_exhausted: freeQuotaExhausted,
      source: CLAIM_GA_SOURCE,
      product_type: PRODUCT_TYPES.SERVERLESS
    })
    eventTracking(MixpanelEvents.ZeroClaim.Create.RequestStarted)
    readySetupInFlightRef.current = false
    setSetupStage('claiming')
    setSetupErrorMessage('')
    setSetupConnectionString('')
    setSetupConsoleContext(null)

    try {
      const claimedCluster = await claimTidb(formData)
      if (!mountedRef.current) {
        return
      }

      const clusterId = claimedCluster?.clusterId || ''
      if (!clusterId) {
        throw new Error('Failed to get claimed cluster ID.')
      }

      const context: ClaimClusterContext = {
        clusterId,
        orgId: formData.orgId,
        projectId: formData.projectId
      }

      setSetupStage('polling')
      setPollingContext(context)
    } catch (error) {
      if (!mountedRef.current) {
        return
      }
      finishSetupWithError(error)
    }
  })

  useEffect(() => {
    reportGA({
      event: 'zero_claim_page_view',
      zero_id: claimId,
      page_category: '/tidbs/claim/:id',
      source: CLAIM_GA_SOURCE,
      product_type: PRODUCT_TYPES.SERVERLESS
    })
    eventTracking(MixpanelEvents.ZeroClaim.View.EnterClicked)
  }, [claimId])

  if (shouldRenderProvisioning) {
    return (
      <Box
        sx={{
          height: '100vh',
          width: '100%',
          overflow: 'auto',
          backgroundColor: 'var(--mantine-color-mustard-2)',
          backgroundImage: `url(${claimBg})`,
          backgroundSize: 'cover',
          backgroundPosition: 'center top',
          backgroundRepeat: 'no-repeat',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center'
        }}
        data-testid="claim-page-provisioning-container"
      >
        <ClaimProvisioning
          stage={renderProvisioningStage}
          errorMessage={setupErrorMessage}
          connectionString={setupConnectionString}
          onOpenConsole={setupStage === 'done' && !!setupConsoleContext ? handleOpenConsole : undefined}
        />
      </Box>
    )
  }

  return (
    <Box
      sx={{
        display: 'flex',
        height: '100vh',
        width: '100%',
        overflow: 'hidden'
      }}
      data-testid="claim-page-container"
    >
      <FormProvider {...formMethods}>
        <Box
          component="main"
          sx={{
            flex: 1,
            overflow: 'auto',
            display: 'flex',
            justifyContent: 'center',
            backgroundColor: 'var(--mantine-color-carbon-1)',
            padding: '80px 40px'
          }}
        >
          <Box sx={{ width: '100%', maxWidth: 520 }}>
            <ClaimForm
              formId={formId}
              onSubmit={formMethods.handleSubmit(handleValidSubmit)}
              orgOptions={orgOptions}
              projectOptionsByOrg={projectOptionsByOrg}
              isScopeLoading={isScopeLoading || isClaimFlowRunning}
              onOrgChange={handleOrgChange}
              onProjectChange={handleProjectChange}
            />
            <ClaimActions
              formId={formId}
              claimId={claimId}
              freeQuotaExhausted={freeQuotaExhausted}
              isPending={isClaimFlowRunning}
              isLoading={isClaimContextLoading || isClaimFlowRunning}
            />
          </Box>
        </Box>
        <Box
          component="aside"
          sx={{
            width: '45%',
            maxWidth: 640,
            flexShrink: 0,
            backgroundColor: 'var(--mantine-color-mustard-2)',
            backgroundImage: `url(${claimBg})`,
            backgroundSize: 'cover',
            backgroundPosition: 'center top',
            backgroundRepeat: 'no-repeat',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: 40
          }}
        >
          <ClaimSummary claimId={claimId} orgNameById={orgNameById} projectNameById={projectNameById} />
        </Box>
      </FormProvider>
    </Box>
  )
})

export default ClaimPage
