/*
 * Copyright 2021-2025 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  Coverage,
  QODANA_LICENSES_JSON,
  QODANA_LICENSES_MD,
  QODANA_OPEN_IN_IDE_NAME,
  QODANA_REPORT_URL_NAME, VERSION,
} from "./qodana";
import * as fs from "fs";
import type {Log, Result} from 'sarif'
import {parseRules, Rule} from './utils'

export const COMMIT_USER = 'qodana-bot'
export const COMMIT_EMAIL = 'qodana-support@jetbrains.com'
export const QODANA_CHECK_NAME = 'Qodana'
const UNKNOWN_RULE_ID = 'Unknown'
const SUMMARY_TABLE_HEADER = '| Inspection name | Severity | Problems |'
const SUMMARY_TABLE_SEP = '| --- | --- | --- |'
const SUMMARY_MISC = `Contact us at [qodana-support@jetbrains.com](mailto:qodana-support@jetbrains.com)
  - Or via our issue tracker: https://jb.gg/qodana-issue
  - Or share your feedback: https://jb.gg/qodana-discussions`

const SUMMARY_PR_MODE = `💡 Qodana analysis was run in the pull request mode: only the changed files were checked`

export const FAILURE_LEVEL = 'failure'
export const WARNING_LEVEL = 'warning'
export const NOTICE_LEVEL = 'notice'

export interface ProblemDescriptor {
  title: string | undefined
  level: 'failure' | 'warning' | 'notice'
}

interface CloudData {
  url?: string
}

interface OpenInIDEData {
  cloud?: CloudData
}

export interface LicenseEntry {
  name?: string
  version?: string
  license?: string
}

export interface LicenseInfo {
  licenses: string,
  packages: number
}

export interface Output {
  title: string
  summary: string
  text: string
  problemDescriptions: ProblemDescriptor[]
}

export function parseSarif(path: string, text: string): Output {
  const sarif: Log = JSON.parse(
    fs.readFileSync(path, {encoding: 'utf8'})
  ) as Log
  const run = sarif.runs[0]
  const rules = parseRules(run.tool)
  let title = 'No new problems found by '
  let problemDescriptions: ProblemDescriptor[] = []
  if (run.results?.length) {
    title = `${run.results.length} ${getProblemPlural(
      run.results.length
    )} found by `
    problemDescriptions = run.results
      .filter(
        result =>
          result.baselineState !== 'unchanged' &&
          result.baselineState !== 'absent'
      )
      .map(result => parseResult(result, rules))
      .filter((a): a is ProblemDescriptor => a !== null && a !== undefined)
  }
  const name = run.tool.driver.fullName || 'Qodana'
  title += name
  return {
    title,
    text: text,
    summary: title,
    problemDescriptions
  }
}

export function parseResult(
  result: Result,
  rules: Map<string, Rule>
): ProblemDescriptor | null {
  if (
    !result.locations ||
    result.locations.length === 0 ||
    !result.locations[0].physicalLocation
  ) {
    return null
  }
  return {
    title: rules.get(result.ruleId!)?.shortDescription,
    level: (() => {
      switch (result.level) {
        case 'error':
          return FAILURE_LEVEL
        case 'warning':
          return WARNING_LEVEL
        default:
          return NOTICE_LEVEL
      }
    })()
  }
}

function wrapToDiffBlock(message: string): string {
  return `\`\`\`diff
${message}
\`\`\``
}

function makeConclusion(
  conclusion: string,
  failedByThreshold: boolean,
  useDiffBlock: boolean,
): string {
  if (useDiffBlock) {
    return failedByThreshold ? `- ${conclusion}` : `+ ${conclusion}`
  } else {
    return failedByThreshold
      ? `<span style="background-color: #ffe6e6; color: red;">${conclusion}</span>`
      : `<span style="background-color: #e6f4e6; color: green;">${conclusion}</span>`
  }
}

export function getCoverageStats(c: Coverage, useDiffBlock: boolean): string {
  if (c.totalLines === 0 && c.totalCoveredLines === 0) {
    return ''
  }

  let stats = ''
  if (c.totalLines !== 0) {
    const conclusion = `${c.totalCoverage}% total lines covered`
    stats += `${makeConclusion(conclusion, c.totalCoverage < c.totalCoverageThreshold, useDiffBlock)}
${c.totalLines} lines analyzed, ${c.totalCoveredLines} lines covered`
  }

  if (c.freshLines !== 0) {
    const conclusion = `${c.freshCoverage}% fresh lines covered`
    stats += `
${makeConclusion(conclusion, c.freshCoverage < c.freshCoverageThreshold, useDiffBlock)}
${c.freshLines} lines analyzed, ${c.freshCoveredLines} lines covered`
  }

  const coverageBlock =  [
    `@@ Code coverage @@`,
    `${stats}`,
    `# Calculated according to the filters of your coverage tool`
  ].join('\n')
  return useDiffBlock ? wrapToDiffBlock(coverageBlock) : coverageBlock
}

export function getLicenseInfo(
  resultsDir: string,
): LicenseInfo {
  let licensesInfo = ''
  let packages = 0
  const licensesJson = `${resultsDir}/projectStructure/${QODANA_LICENSES_JSON}`
  if (fs.existsSync(licensesJson)) {
    const licenses = JSON.parse(
      fs.readFileSync(licensesJson, {encoding: 'utf8'})
    ) as LicenseEntry[]
    if (licenses.length > 0) {
      packages = licenses.length
      licensesInfo = fs.readFileSync(
        `${resultsDir}/projectStructure/${QODANA_LICENSES_MD}`,
        {encoding: 'utf8'}
      )
    }
  }
  return { licenses: licensesInfo, packages: packages }
}

export function getReportURL(resultsDir: string): string {
  let reportUrlFile = `${resultsDir}/${QODANA_OPEN_IN_IDE_NAME}`
  if (fs.existsSync(reportUrlFile)) {
    const rawData = fs.readFileSync(reportUrlFile, {encoding: 'utf8'})
    const data = JSON.parse(rawData) as OpenInIDEData
    if (data?.cloud?.url) {
      return data.cloud.url
    }
  } else {
    reportUrlFile = `${resultsDir}/${QODANA_REPORT_URL_NAME}`
    if (fs.existsSync(reportUrlFile)) {
      return fs.readFileSync(reportUrlFile, {encoding: 'utf8'})
    }
  }
  return ''
}

function wrapToToggleBlock(header: string, body: string): string {
  return `<details>
<summary>${header}</summary>

${body}
</details>`
}

function getViewReportText(reportUrl: string, viewReportOptions: string): string {
  if (reportUrl !== '') {
    return `☁️ [View the detailed Qodana report](${reportUrl})`
  }
  return wrapToToggleBlock(
    'View the detailed Qodana report',
    viewReportOptions
  )
}

/**
 * Generates a table row for a given level.
 * @param annotations The annotations to generate the table row from.
 * @param level The level to generate the table row for.
 */
function getRowsByLevel(annotations: ProblemDescriptor[], level: string): string {
  const problems = annotations.reduce(
    (map: Map<string, number>, e) =>
      map.set(
        e.title ?? UNKNOWN_RULE_ID,
        map.get(e.title ?? UNKNOWN_RULE_ID) !== undefined
          ? map.get(e.title ?? UNKNOWN_RULE_ID)! + 1
          : 1
      ),
    new Map()
  )
  return Array.from(problems.entries())
    .sort((a, b) => b[1] - a[1])
    .map(([title, count]) => `| \`${title}\` | ${level} | ${count} |`)
    .join('\n')
}

/**
 * Generates action summary string of annotations.
 * @param toolName The name of the tool to generate the summary from.
 * @param projectDir The path to the project.
 * @param sourceDir The path to analyzed directory inside the project.
 * @param problemsDescriptors The descriptions of problems to generate the summary from.
 * @param coverageInfo The coverage is a Markdown text to generate the summary from.
 * @param packages The number of dependencies in the analyzed project.
 * @param licensesInfo The licenses a Markdown text to generate the summary from.
 * @param reportUrl The URL to the Qodana report.
 * @param prMode Whether the analysis was run in the pull request mode.
 * @param dependencyCharsLimit Limit on how many characters can be included in comment
 * @param reportViewOptionsHelp Instructions of how to configure a report viewing for tool
 */
export function getSummary(
  toolName: string,
  projectDir: string,
  sourceDir: string,
  problemsDescriptors: ProblemDescriptor[],
  coverageInfo: string,
  packages: number,
  licensesInfo: string,
  reportUrl: string,
  prMode: boolean,
  dependencyCharsLimit: number,
  reportViewOptionsHelp: string
): string {
  const contactBlock = wrapToToggleBlock('Contact Qodana team', SUMMARY_MISC)
  let licensesBlock = ''
  if (licensesInfo !== '' && licensesInfo.length < dependencyCharsLimit) {
    licensesBlock = wrapToToggleBlock(
      `Detected ${packages} ${getDepencencyPlural(packages)}`,
      licensesInfo
    )
  }
  let prModeBlock = ''
  if (prMode) {
    prModeBlock = SUMMARY_PR_MODE
  }
  if (reportUrl !== '') {
    const firstToolName = toolName.split(' ')[0]
    toolName = toolName.replace(
      firstToolName,
      `[${firstToolName}](${reportUrl})`
    )
  }
  const analysisScope = (
    projectDir === ''
      ? ''
      : ['Analyzed project: `', projectDir, '/`\n'].join('')
  ).concat(
    sourceDir === ''
      ? ''
      : ['Analyzed directory: `', sourceDir, '/`\n'].join('')
  )
  if (problemsDescriptors.length === 0) {
    return [
      `# ${toolName}`,
      analysisScope,
      '**It seems all right 👌**',
      '',
      'No new problems were found according to the checks applied',
      coverageInfo,
      prModeBlock,
      getViewReportText(reportUrl, reportViewOptionsHelp),
      licensesBlock,
      contactBlock
    ].join('\n')
  }

  return [
    `# ${toolName}`,
    analysisScope,
    `**${problemsDescriptors.length} ${getProblemPlural(
      problemsDescriptors.length
    )}** were found`,
    '',
    SUMMARY_TABLE_HEADER,
    SUMMARY_TABLE_SEP,
    [
      getRowsByLevel(
        problemsDescriptors.filter(a => a.level === FAILURE_LEVEL),
        '🔴 Failure'
      ),
      getRowsByLevel(
        problemsDescriptors.filter(a => a.level === WARNING_LEVEL),
        '🔶 Warning'
      ),
      getRowsByLevel(
        problemsDescriptors.filter(a => a.level === NOTICE_LEVEL),
        '◽️ Notice'
      )
    ]
      .filter(e => e !== '')
      .join('\n'),
    '',
    coverageInfo,
    prModeBlock,
    getViewReportText(reportUrl, reportViewOptionsHelp),
    licensesBlock,
    contactBlock
  ].join('\n')
}

/**
 * Generates a plural form of the word "problem" depending on the given count.
 * @param count A number representing the count of problems
 * @returns A formatted string with the correct plural form of "problem"
 */
export function getProblemPlural(count: number): string {
  return `new problem${count !== 1 ? 's' : ''}`
}

/**
 * Generates a plural form of the word "dependency" depending on the given count.
 * @param count A number representing the count of dependencies
 * @returns A formatted string with the correct plural form of "dependency"
 */
export function getDepencencyPlural(count: number): string {
  return `dependenc${count !== 1 ? 'ies' : 'y'}`
}

export function getCommentTag(toolName: string, sourceDir: string): string {
  // source dir needed in case of monorepo with projects analyzed by the same tool
  return `<!-- JetBrains/qodana-action@v${VERSION} : ${toolName}, ${sourceDir} -->`
}