import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useGet, useMutate } from 'restful-react'
import { useParams } from 'react-router-dom'
import { get, isEmpty, isUndefined, set } from 'lodash-es'
import { parse, stringify } from 'yaml'
import cx from 'classnames'
import { Menu, PopoverPosition } from '@blueprintjs/core'
import { Container, PageBody, Layout, ButtonVariation, Text, useToaster, SplitButton, Button } from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import type { OpenapiCommitFilesRequest, RepoCommitFilesResponse, RepoFileContent, TypesPipeline } from 'services/code'
import { useStrings } from 'framework/strings'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useGetResourceContent } from 'hooks/useGetResourceContent'
import MonacoSourceCodeEditor from 'components/SourceCodeEditor/MonacoSourceCodeEditor'
import { PluginForm, PluginsPanel } from 'components/PluginsPanel/PluginsPanel'
import useRunPipelineModal from 'components/RunPipelineModal/RunPipelineModal'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { useAppContext } from 'AppContext'
import type { CODEProps } from 'RouteDefinitions'
import { getErrorMessage } from 'utils/Utils'
import { decodeGitContent } from 'utils/GitUtils'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import pipelineSchemaV1 from './schema/pipeline-schema-v1.json'
import pipelineSchemaV0 from './schema/pipeline-schema-v0.json'
import { DRONE_CONFIG_YAML_FILE_SUFFIXES, YamlVersion } from './Constants'

import css from './AddUpdatePipeline.module.scss'

const StarterPipelineV1: Record<string, unknown> = {
  version: 1,
  kind: 'pipeline',
  spec: {
    stages: [
      {
        name: 'build',
        type: 'ci',
        spec: {
          steps: [
            {
              name: 'build',
              type: 'run',
              spec: {
                container: 'alpine',
                script: 'echo "hello world"'
              }
            }
          ]
        }
      }
    ]
  }
}

const StarterPipelineV0: Record<string, unknown> = {
  kind: 'pipeline',
  type: 'docker',
  name: 'default',
  steps: [
    {
      name: 'test',
      image: 'alpine',
      commands: ['echo hello world']
    }
  ]
}

enum PipelineSaveAndRunAction {
  SAVE,
  RUN,
  SAVE_AND_RUN
}

interface PipelineSaveAndRunOption {
  title: string
  action: PipelineSaveAndRunAction
}

const AddUpdatePipeline = (): JSX.Element => {
  const { routes } = useAppContext()
  const { getString } = useStrings()
  const { pipeline } = useParams<CODEProps>()
  const { repoMetadata } = useGetRepositoryMetadata()
  const { showError, showSuccess, clear: clearToaster } = useToaster()
  const [yamlVersion, setYAMLVersion] = useState<YamlVersion>()
  const [pipelineAsYAML, setPipelineAsYaml] = useState<string>('')
  const { openModal: openRunPipelineModal } = useRunPipelineModal()
  const repoPath = useMemo(() => repoMetadata?.path || '', [repoMetadata])
  const [isExistingPipeline, setIsExistingPipeline] = useState<boolean>(false)
  const [isDirty, setIsDirty] = useState<boolean>(false)
  const [generatingPipeline, setGeneratingPipeline] = useState<boolean>(false)
  const pipelineAsYAMLRef = useRef<string>('')

  const pipelineSaveOption: PipelineSaveAndRunOption = {
    title: getString('save'),
    action: PipelineSaveAndRunAction.SAVE
  }

  const pipelineRunOption: PipelineSaveAndRunOption = {
    title: getString('run'),
    action: PipelineSaveAndRunAction.RUN
  }

  const pipelineSaveAndRunOption: PipelineSaveAndRunOption = {
    title: getString('pipelines.saveAndRun'),
    action: PipelineSaveAndRunAction.SAVE_AND_RUN
  }

  const pipelineSaveAndRunOptions: PipelineSaveAndRunOption[] = [pipelineSaveAndRunOption, pipelineSaveOption]

  const [selectedOption, setSelectedOption] = useState<PipelineSaveAndRunOption>()

  const { mutate, loading } = useMutate<RepoCommitFilesResponse>({
    verb: 'POST',
    path: `/api/v1/repos/${repoPath}/+/commits`
  })

  // Fetch pipeline metadata to fetch pipeline YAML file content
  const { data: pipelineData, loading: fetchingPipeline } = useGet<TypesPipeline>({
    path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`,
    lazy: !repoMetadata
  })

  const {
    data: pipelineYAMLFileContent,
    loading: fetchingPipelineYAMLFileContent,
    refetch: fetchPipelineYAMLFileContent
  } = useGetResourceContent({
    repoMetadata,
    gitRef: pipelineData?.default_branch || '',
    resourcePath: pipelineData?.config_path || ''
  })

  const originalPipelineYAMLFileContent = useMemo(
    (): string => decodeGitContent((pipelineYAMLFileContent?.content as RepoFileContent)?.data),
    [pipelineYAMLFileContent?.content]
  )

  // set YAML version for Pipeline setup
  useEffect(() => {
    setYAMLVersion(
      DRONE_CONFIG_YAML_FILE_SUFFIXES.find((suffix: string) => pipelineData?.config_path?.endsWith(suffix))
        ? YamlVersion.V0
        : YamlVersion.V1
    )
  }, [pipelineData])

  // check if file already exists and has some content
  useEffect(() => {
    setIsExistingPipeline(!isEmpty(originalPipelineYAMLFileContent) && !isUndefined(originalPipelineYAMLFileContent))
  }, [originalPipelineYAMLFileContent])

  // load initial content on the editor
  useEffect(() => {
    if (isExistingPipeline) {
      setPipelineAsYaml(originalPipelineYAMLFileContent)
    } else {
      // load with starter pipeline
      try {
        setPipelineAsYaml(stringify(yamlVersion === YamlVersion.V1 ? StarterPipelineV1 : StarterPipelineV0))
      } catch (ex) {
        // ignore exception
      }
    }
  }, [yamlVersion, isExistingPipeline, originalPipelineYAMLFileContent])

  // find if editor content was modified
  useEffect(() => {
    setIsDirty(originalPipelineYAMLFileContent !== pipelineAsYAML)
  }, [originalPipelineYAMLFileContent, pipelineAsYAML])

  // set initial CTA title
  useEffect(() => {
    setSelectedOption(isDirty ? pipelineSaveAndRunOption : pipelineRunOption)
  }, [isDirty])

  const handleSaveAndRun = (option: PipelineSaveAndRunOption): void => {
    if ([PipelineSaveAndRunAction.SAVE_AND_RUN, PipelineSaveAndRunAction.SAVE].includes(option?.action)) {
      try {
        const data: OpenapiCommitFilesRequest = {
          actions: [
            {
              action: isExistingPipeline ? 'UPDATE' : 'CREATE',
              path: pipelineData?.config_path,
              payload: pipelineAsYAML,
              sha: isExistingPipeline ? pipelineYAMLFileContent?.sha : ''
            }
          ],
          branch: pipelineData?.default_branch || '',
          title: `${isExistingPipeline ? getString('updated') : getString('created')} pipeline ${pipeline}`,
          message: ''
        }

        mutate(data)
          .then(() => {
            fetchPipelineYAMLFileContent()
            clearToaster()
            showSuccess(getString(isExistingPipeline ? 'pipelines.updated' : 'pipelines.created'))
            if (option?.action === PipelineSaveAndRunAction.SAVE_AND_RUN && repoMetadata && pipeline) {
              openRunPipelineModal({ repoMetadata, pipeline })
            }
            setSelectedOption(pipelineRunOption)
          })
          .catch(error => {
            showError(getErrorMessage(error), 0, 'pipelines.failedToSavePipeline')
          })
      } catch (exception) {
        showError(getErrorMessage(exception), 0, 'pipelines.failedToSavePipeline')
      }
    }
  }

  const updatePipelineWithPluginData = (
    existingPipeline: Record<string, unknown>,
    payload: Record<string, unknown>
  ): Record<string, unknown> => {
    const pipelineAsObjClone = { ...existingPipeline }
    if (Object.keys(pipelineAsObjClone).length > 0) {
      const stepInsertPath = 'spec.stages.0.spec.steps'
      let existingSteps = get(pipelineAsObjClone, stepInsertPath, []) as unknown[]
      if (existingSteps.length > 0) {
        existingSteps.push(payload)
      } else {
        existingSteps = [payload]
      }
      set(pipelineAsObjClone, stepInsertPath, existingSteps)
      return pipelineAsObjClone
    }
    return existingPipeline
  }

  const handlePluginAddUpdateToPipeline = useCallback(
    ({
      pluginFormData,
      existingYAML
    }: {
      isUpdate: boolean
      pluginFormData: PluginForm
      existingYAML: string
    }): void => {
      try {
        const pipelineAsObj = parse(existingYAML)
        const updatedPipelineAsObj = updatePipelineWithPluginData(pipelineAsObj, pluginFormData)
        if (Object.keys(updatedPipelineAsObj).length > 0) {
          // avoid setting to empty pipeline in case pipeline update with plugin data fails
          const updatedPipelineAsYAML = stringify(updatedPipelineAsObj)
          setPipelineAsYaml(updatedPipelineAsYAML)
          pipelineAsYAMLRef.current = updatedPipelineAsYAML
        }
      } catch (ex) {
        // ignore exception
      }
    },
    []
  )

  const handleGeneratePipeline = useCallback(async (): Promise<void> => {
    try {
      const response = await fetch(`/api/v1/repos/${repoPath}/+/pipelines/generate`)
      if (response.ok && response.status === 200) {
        const responsePipelineAsYAML = await response.text()
        if (responsePipelineAsYAML) {
          setPipelineAsYaml(responsePipelineAsYAML)
        }
      }
      setGeneratingPipeline(false)
    } catch (exception) {
      showError(getErrorMessage(exception), 0, getString('pipelines.failedToGenerate'))
      setGeneratingPipeline(false)
    }
  }, [repoPath])

  const renderCTA = useCallback(() => {
    /* Do not render CTA till pipeline existence info is obtained */
    if (fetchingPipeline || !pipelineData) {
      return <></>
    }
    switch (selectedOption?.action) {
      case PipelineSaveAndRunAction.RUN:
        return (
          <Button
            variation={ButtonVariation.PRIMARY}
            text={getString('run')}
            onClick={() => {
              if (repoMetadata && pipeline) {
                openRunPipelineModal({ repoMetadata, pipeline })
              }
            }}
          />
        )
      case PipelineSaveAndRunAction.SAVE:
      case PipelineSaveAndRunAction.SAVE_AND_RUN:
        return isExistingPipeline ? (
          <Button
            variation={ButtonVariation.PRIMARY}
            text={getString('save')}
            onClick={() => {
              handleSaveAndRun(pipelineSaveOption)
            }}
            disabled={loading || !isDirty}
          />
        ) : (
          <SplitButton
            text={
              <Text color={Color.WHITE} font={{ variation: FontVariation.BODY2_SEMI, weight: 'bold' }}>
                {pipelineSaveAndRunOptions[0].title}
              </Text>
            }
            disabled={loading || !isDirty}
            variation={ButtonVariation.PRIMARY}
            popoverProps={{
              interactionKind: 'click',
              usePortal: true,
              position: PopoverPosition.BOTTOM_RIGHT,
              popoverClassName: css.popover
            }}
            intent="primary"
            onClick={() => handleSaveAndRun(pipelineSaveAndRunOptions[0])}>
            {[pipelineSaveAndRunOptions[1]].map(option => {
              return (
                <Menu.Item
                  className={css.menuItem}
                  key={option.title}
                  text={<Text font={{ variation: FontVariation.BODY2 }}>{option.title}</Text>}
                  onClick={() => {
                    handleSaveAndRun(option)
                    setSelectedOption(option)
                  }}
                />
              )
            })}
          </SplitButton>
        )
      default:
        return <></>
    }
  }, [
    loading,
    fetchingPipeline,
    fetchingPipelineYAMLFileContent,
    isDirty,
    repoMetadata,
    pipeline,
    selectedOption,
    isExistingPipeline,
    pipelineAsYAML,
    pipelineData
  ])

  if (fetchingPipeline || fetchingPipelineYAMLFileContent) {
    return <LoadingSpinner visible={true} />
  }

  return (
    <>
      <Container className={css.main}>
        <RepositoryPageHeader
          repoMetadata={repoMetadata}
          title={getString('pageTitle.executions')}
          dataTooltipId="repositoryExecutions"
          extraBreadcrumbLinks={
            repoMetadata && [
              {
                label: getString('pageTitle.pipelines'),
                url: routes.toCODEPipelines({ repoPath: repoMetadata.path as string })
              },
              ...(pipeline
                ? [
                    {
                      label: pipeline,
                      url: ''
                    }
                  ]
                : [])
            ]
          }
          content={<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>{renderCTA()}</Layout.Horizontal>}
        />
        <PageBody>
          <Layout.Vertical>
            {!isExistingPipeline && yamlVersion === YamlVersion.V1 && (
              <Layout.Horizontal
                padding={{ left: 'medium', bottom: 'medium', top: 'medium' }}
                className={css.generateHeader}
                spacing="large"
                flex={{ justifyContent: 'flex-start' }}>
                <Button
                  text={getString('generate')}
                  variation={ButtonVariation.SECONDARY}
                  className={css.generate}
                  onClick={handleGeneratePipeline}
                  disabled={generatingPipeline}
                />
                <Text font={{ weight: 'bold' }}>{getString('generateHelptext')}</Text>
              </Layout.Horizontal>
            )}
            <Layout.Horizontal className={css.layout}>
              <Container
                className={cx(css.editorContainer, {
                  [css.extendedHeight]: isExistingPipeline || yamlVersion === YamlVersion.V0
                })}>
                <MonacoSourceCodeEditor
                  language={'yaml'}
                  schema={yamlVersion === YamlVersion.V1 ? pipelineSchemaV1 : pipelineSchemaV0}
                  source={pipelineAsYAML}
                  onChange={(value: string) => setPipelineAsYaml(value)}
                />
              </Container>
              {yamlVersion === YamlVersion.V1 && (
                <Container className={cx(css.pluginsContainer, { [css.extendedHeight]: isExistingPipeline })}>
                  <PluginsPanel
                    onPluginAddUpdate={(isUpdate: boolean, pluginFormData: PluginForm) =>
                      handlePluginAddUpdateToPipeline({
                        isUpdate,
                        pluginFormData,
                        existingYAML: pipelineAsYAMLRef.current || pipelineAsYAML
                      })
                    }
                  />
                </Container>
              )}
            </Layout.Horizontal>
          </Layout.Vertical>
        </PageBody>
      </Container>
    </>
  )
}

export default AddUpdatePipeline
