/*
 * SonarQube
 * Copyright (C) 2009-2021 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
import { flatten, sortBy } from 'lodash';
import BugIcon from '../components/icons/BugIcon';
import CodeSmellIcon from '../components/icons/CodeSmellIcon';
import SecurityHotspotIcon from '../components/icons/SecurityHotspotIcon';
import VulnerabilityIcon from '../components/icons/VulnerabilityIcon';
import { IssueType, RawIssue } from '../types/issues';
import { MetricKey } from '../types/metrics';
import { ISSUE_TYPES } from './constants';

interface Rule {}

interface Component {
  key: string;
  name: string;
}

export function sortByType<T extends Pick<T.Issue, 'type'>>(issues: T[]): T[] {
  return sortBy(issues, issue => ISSUE_TYPES.indexOf(issue.type));
}

function injectRelational(
  issue: T.Dict<any>,
  source: any[] | undefined,
  baseField: string,
  lookupField: string
) {
  const newFields: T.Dict<any> = {};
  const baseValue = issue[baseField];
  if (baseValue !== undefined && source !== undefined) {
    const lookupValue = source.find(candidate => candidate[lookupField] === baseValue);
    if (lookupValue != null) {
      Object.keys(lookupValue).forEach(key => {
        const newKey = baseField + key.charAt(0).toUpperCase() + key.slice(1);
        newFields[newKey] = lookupValue[key];
      });
    }
  }
  return newFields;
}

function injectCommentsRelational(issue: RawIssue, users?: T.UserBase[]) {
  if (!issue.comments) {
    return {};
  }
  const comments = issue.comments.map(comment => {
    const commentWithAuthor = { ...comment, author: comment.login, login: undefined };
    return {
      ...commentWithAuthor,
      ...injectRelational(commentWithAuthor, users, 'author', 'login')
    };
  });
  return { comments };
}

function prepareClosed(
  issue: RawIssue,
  secondaryLocations: T.FlowLocation[],
  flows: T.FlowLocation[][]
) {
  return issue.status === 'CLOSED'
    ? { flows: [], line: undefined, textRange: undefined, secondaryLocations: [] }
    : { flows, secondaryLocations };
}

function ensureTextRange(issue: RawIssue): { textRange?: T.TextRange } {
  return issue.line && !issue.textRange
    ? {
        textRange: {
          startLine: issue.line,
          endLine: issue.line,
          startOffset: 0,
          endOffset: 999999
        }
      }
    : {};
}

function reverseLocations(locations: T.FlowLocation[]): T.FlowLocation[] {
  const x = [...locations];
  x.reverse();
  return x;
}

function splitFlows(
  issue: RawIssue,
  components: Component[] = []
): { secondaryLocations: T.FlowLocation[]; flows: T.FlowLocation[][] } {
  const parsedFlows: T.FlowLocation[][] = (issue.flows || [])
    .filter(flow => flow.locations !== undefined)
    .map(flow => flow.locations!.filter(location => location.textRange != null))
    .map(flow =>
      flow.map(location => {
        const component = components.find(component => component.key === location.component);
        return { ...location, componentName: component && component.name };
      })
    );

  const onlySecondaryLocations = parsedFlows.every(flow => flow.length === 1);

  return onlySecondaryLocations
    ? { secondaryLocations: orderLocations(flatten(parsedFlows)), flows: [] }
    : { secondaryLocations: [], flows: parsedFlows.map(reverseLocations) };
}

function orderLocations(locations: T.FlowLocation[]) {
  return sortBy(
    locations,
    location => location.textRange && location.textRange.startLine,
    location => location.textRange && location.textRange.startOffset
  );
}

export function parseIssueFromResponse(
  issue: RawIssue,
  components?: Component[],
  users?: T.UserBase[],
  rules?: Rule[]
): T.Issue {
  const { secondaryLocations, flows } = splitFlows(issue, components);
  return {
    ...issue,
    ...injectRelational(issue, components, 'component', 'key'),
    ...injectRelational(issue, components, 'project', 'key'),
    ...injectRelational(issue, components, 'subProject', 'key'),
    ...injectRelational(issue, rules, 'rule', 'key'),
    ...injectRelational(issue, users, 'assignee', 'login'),
    ...injectCommentsRelational(issue, users),
    ...prepareClosed(issue, secondaryLocations, flows),
    ...ensureTextRange(issue)
  } as T.Issue;
}

export const ISSUETYPE_METRIC_KEYS_MAP = {
  [IssueType.CodeSmell]: {
    metric: MetricKey.code_smells,
    newMetric: MetricKey.new_code_smells,
    rating: MetricKey.sqale_rating,
    newRating: MetricKey.new_maintainability_rating,
    ratingName: 'Maintainability',
    iconClass: CodeSmellIcon
  },
  [IssueType.Vulnerability]: {
    metric: MetricKey.vulnerabilities,
    newMetric: MetricKey.new_vulnerabilities,
    rating: MetricKey.security_rating,
    newRating: MetricKey.new_security_rating,
    ratingName: 'Security',
    iconClass: VulnerabilityIcon
  },
  [IssueType.Bug]: {
    metric: MetricKey.bugs,
    newMetric: MetricKey.new_bugs,
    rating: MetricKey.reliability_rating,
    newRating: MetricKey.new_reliability_rating,
    ratingName: 'Reliability',
    iconClass: BugIcon
  },
  [IssueType.SecurityHotspot]: {
    metric: MetricKey.security_hotspots,
    newMetric: MetricKey.new_security_hotspots,
    rating: MetricKey.security_review_rating,
    newRating: MetricKey.new_security_review_rating,
    ratingName: 'SecurityReview',
    iconClass: SecurityHotspotIcon
  }
};