import { AxiosInstance } from 'axios'
import { Artifact, CustomReportArtifact, createAxios } from './client'
import { minBy } from 'lodash'
import minimatch from 'minimatch'
import { CustomReportConfig } from '../config/config'
import { Logger } from 'tslog'
import { ArgumentOptions } from '../arg_options'

// ref: https://github.com/jenkinsci/pipeline-stage-view-plugin/blob/master/rest-api/src/main/java/com/cloudbees/workflow/rest/external/StatusExt.java
export type JenkinsStatus = 'SUCCESS' | 'FAILED' | 'ABORTED' | 'NOT_EXECUTED' | 'IN_PROGRESS' | 'PAUSED_PENDING_INPUT' | 'UNSTABLE'

type JobResponse = {
  _class : string
  name: string
  url: string
  color: string
}

export type WfapiRunResponse = {
  _links: {
    self: {
      href: string // "/job/ci_analyzer/7/wfapi/describe"
    },
    artifacts: {
      href: string // "/job/ci_analyzer/7/wfapi/artifacts"
    }
  },
  id: string // '16',
  name: string // '#16',
  status: JenkinsStatus // 'SUCCESS',
  startTimeMillis: number // 1588392912311,
  endTimeMillis: number // 1588393121925,
  durationMillis: number // 209614,
  queueDurationMillis: number // 3,
  pauseDurationMillis: number // 0,
  stages: Stage[]
}

type Stage = {
  _links: {
    self: {
      href: string // "/job/ci_analyzer/9/execution/node/6/wfapi/describe"
    }
  },
  id: string // "6",
  name: string // "Declarative: Checkout SCM",
  execNode: string // "",
  status: JenkinsStatus // "SUCCESS",
  error?: {
    message: string // "script returned exit code 243",
    type: string // "hudson.AbortException"
  }
  startTimeMillis: number // 1588343995998,
  durationMillis: number // 1410,
  pauseDurationMillis: number // 0,
  stageFlowNodes: StageFlowNode[]
}

type StageFlowNode = {
  _links: {
    self: {
      href: string // "/job/ci_analyzer/9/execution/node/7/wfapi/describe"
    },
    log: {
      href: string // "/job/ci_analyzer/9/execution/node/7/wfapi/log"
    },
    console: {
      href: string // "/job/ci_analyzer/9/execution/node/7/log"
    }
  }
  id: string // "7",
  name: string // "Check out from version control",
  execNode: string // "",
  status: JenkinsStatus // "SUCCESS",
  error?: {
    message: string // "script returned exit code 1",
    type: string // "hudson.AbortException"
  }
  parameterDescription?: string // "docker pull node:lts",
  startTimeMillis: number // 1588343996020,
  durationMillis: number // 1377,
  pauseDurationMillis: number // 0,
  parentNodes: string[] // [ "6" ]
}

export type BuildResponse = {
  id: string // '80'
  number: number // 80
  fullDisplayName: string // "ci_analyzer #90",
  timestamp: number // 1605412528346 (milisec timestamp)
  actions: (
    CauseAction |
    BuildData |
    GhprbParametersAction | // from GitHub Pull Request Builder plugin
    ParametersAction |
    TimeInQueueAction // from Metrics plugin
  )[]
  artifacts: {
    displayPath: string // "junit.xml",
    fileName: string // "junit.xml",
    relativePath: string // "junit/junit.xml"
  }[]
}

export type CauseAction = {
  _class: "hudson.model.CauseAction"
  causes: {
    "_class":
      "hudson.model.Cause$UserIdCause" |
      "hudson.triggers.SCMTrigger$SCMTriggerCause" |
      "org.jenkinsci.plugins.ghprb.GhprbCause" |
      "hudson.triggers.TimerTrigger$TimerTriggerCause"
  }[]
}

export type BuildData = {
  _class: "hudson.plugins.git.util.BuildData"
  lastBuiltRevision: {
    SHA1: string // "9db2c418143a661d07b7458debe0b7bced0cdb47"
    branch: {
      SHA1: string // "9db2c418143a661d07b7458debe0b7bced0cdb47"
      name: string // "refs/remotes/origin/feature/jenkinsfile"
    }[]
  }
  remoteUrls: string[] // "https://github.com/Kesin11/CIAnalyzer.git"
}

export type GhprbParametersAction = {
  _class: "org.jenkinsci.plugins.ghprb.GhprbParametersAction"
  parameters: (
    {
      _class: "hudson.model.StringParameterValue"
      name: "ghprbActualCommit"
      value: string // "ee0f40fec52d69d2961a726c1284002675b3d68a"
    } |
    {
      _class: "hudson.model.StringParameterValue"
      name: "ghprbAuthorRepoGitUrl"
      value: string // "https://github.com/Kesin11/CIAnalyzer.git"
    } |
    {
      _class: "hudson.model.StringParameterValue"
      name: "GIT_BRANCH"
      value: string // "feature/jenkinsfile"
    } |
    {
      _class: "hudson.model.StringParameterValue"
      name: "ghprbGhRepository"
      value: string // "Kesin11/CIAnalyzer"
    }
  )[]
}

export type ParametersAction = {
  _class: "hudson.model.ParametersAction"
  parameters:
    {
      _class: string // ex: "hudson.model.StringParameterValue"
      name: string // "TIMEOUT",
      value?: string | number | boolean // "10"
    }[]
}

export type TimeInQueueAction = {
  _class : "jenkins.metrics.impl.TimeInQueueAction",
  blockedDurationMillis : number,
  blockedTimeMillis : number,
  buildableDurationMillis : number, // Freestyle job queued time
  buildableTimeMillis : number, // Pipeline job queued time
  buildingDurationMillis : number,
  executingTimeMillis : number,
  executorUtilization : number,
  subTaskCount : number,
  waitingDurationMillis : number,
  waitingTimeMillis : number
}

export class JenkinsClient {
  private axios: AxiosInstance
  constructor(baseUrl: string, logger: Logger, private options: ArgumentOptions, user?: string, token?: string) {
    if ((user && !token) || (!user && token)) throw new Error('Either $JENKSIN_USER or $JENKINS_TOKEN is undefined.')

    const auth = (user && token) ? {
      username: user,
      password: token,
    } : undefined

    const axiosLogger = logger.getChildLogger({ name: JenkinsClient.name })
    this.axios = createAxios(axiosLogger, options, {
      baseURL: baseUrl,
      auth,
    })
  }

  async fetchJobs() {
    const res = await this.axios.get("api/json")

    const jobs = res.data.jobs as JobResponse[]
    return jobs.filter((job) => {
      return job._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob"
    })
  }

  async fetchJobRuns(jobName: string, lastRunId?: number) {
    const url = encodeURI(`job/${jobName}/wfapi/runs`)
    let runs: WfapiRunResponse[]
    try {
      const res = await this.axios.get(url, {
        params: {
          fullStages: "true"
        }
      })
      runs = res.data
    }
    // Sometimes wfapi/runs return 500.
    // However if that job comes from `correctAllJobs` config option, user may not do anything.
    // To handle this situation, catch the error and return empty array.
    catch { return [] }

    return this.filterJobRuns(runs, lastRunId)
  }

  // Filter to: lastRunId < Id < firstInprogressId
  filterJobRuns (runs: WfapiRunResponse[], lastRunId?: number): WfapiRunResponse[] {
    runs = (lastRunId)
      ? runs.filter((run) => Number(run.id) > lastRunId)
      : runs
    const firstInprogress = minBy(
      runs.filter((run) => run.status === 'IN_PROGRESS' ),
      (run) => Number(run.id)
    )
    runs = (firstInprogress)
      ? runs.filter((run) => Number(run.id) < Number(firstInprogress.id))
      : runs
    return runs
  }

  async fetchJobRun(jobName: string, runId: number) {
    const url = encodeURI(`job/${jobName}/${runId}/wfapi/describe`)
    const res = await this.axios.get(url)

    return res.data as WfapiRunResponse
  }

  async fetchBuild(jobName: string, runId: number) {
    const url = encodeURI(`job/${jobName}/${runId}/api/json`)
    const res = await this.axios.get(url)

    return res.data as BuildResponse
  }

  async fetchLastBuild(jobName: string) {
    const url = encodeURI(`/job/${jobName}/lastBuild/api/json`)
    const res = await this.axios.get(url)

    return res.data as BuildResponse
  }

  async fetchArtifacts(jobName: string, runId: number, paths: string[]): Promise<Artifact[]> {
    const pathResponses = paths.map((path) => {
      const url = encodeURI(`job/${jobName}/${runId}/artifact/${path}`)
      const response = this.axios.get(url, { responseType: 'arraybuffer'})
      return { path, response }
    })

    const artifacts = []
    for (const { path, response } of pathResponses) {
      artifacts.push({
        path,
        data: (await response).data as ArrayBuffer
      })
    }
    return artifacts
  }

  async fetchTests(build: BuildResponse, globs: string[]): Promise<Artifact[]> {
    // Skip if test file globs not provided
    if (globs.length < 1) return []

    const artifactPaths = build.artifacts.map((artifact) => artifact.relativePath )
    const testPaths = artifactPaths.filter((path) => {
      return globs.some((glob) => minimatch(path, glob))
    })

    const jobName = build.fullDisplayName.split(' ')[0]
    return this.fetchArtifacts(jobName, build.number, testPaths)
  }

  async fetchCustomReports(build: BuildResponse, customReportsConfigs: CustomReportConfig[]): Promise<CustomReportArtifact> {
    // Skip if custom report config are not provided
    if (customReportsConfigs.length < 1) return new Map()

    const artifactPaths = build.artifacts.map((artifact) => artifact.relativePath )
    const jobName = build.fullDisplayName.split(' ')[0]

    // Fetch artifacts in parallel
    const customReports: CustomReportArtifact = new Map<string, Artifact[]>()
    const nameArtifacts = customReportsConfigs.map((customReportConfig) => {
      const reportArtifactsPaths = artifactPaths.filter((path) => {
        return customReportConfig.paths.some((glob) => minimatch(path, glob))
      })
      return {
        name: customReportConfig.name,
        artifacts: this.fetchArtifacts(jobName, build.number, reportArtifactsPaths)
      }
    })
    for (const { name, artifacts } of nameArtifacts) {
      customReports.set(name, await artifacts)
    }

    return customReports
  }
}