package com.pingcap.kb.integration.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy;
import com.pingcap.kb.integration.dao.JiraIssueDao;
import com.pingcap.kb.integration.dao.KbPostBindingDao;
import com.pingcap.kb.integration.dao.KbPostStatusDao;
import com.pingcap.kb.integration.entity.JiraIssueStatus;
import com.pingcap.kb.integration.entity.KbPostStatus;
import com.pingcap.kb.integration.jira.JiraClient;
import com.pingcap.kb.integration.jira.JiraIssue;
import com.pingcap.kb.integration.jira.JiraIssue.Fields;
import com.pingcap.kb.integration.jira.JiraUpdate;
import com.pingcap.kb.integration.kb.KbClient;
import com.pingcap.kb.integration.kb.Post.Meta;
import com.pingcap.kb.integration.kb.Solution;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Slf4j
@AllArgsConstructor
@Service
public class IntegrationService {

  private final KbClient kbClient;
  private final JiraClient jiraClient;
  private final KbPostBindingDao kbPostBindingDao;
  private final KbPostStatusDao kbPostStatusDao;
  private final JiraIssueDao jiraIssueDao;
  private final ObjectMapper objectMapper;

  /**
   * Track jira issue, create kb material, remove `kb-candidate` and add `kb-progress`
   *
   * @param issue tracking jira issue
   */
  public void trackIssue(JiraIssue issue) {
    Optional<JiraIssueStatus> oJis = jiraIssueDao.findByKey(issue.getKey());
    JiraIssueStatus jis;
    Fields fields = issue.getFields();
    if (oJis.isPresent()) {
      jis = oJis.get();
      if (!jis.isTracking()) {
        // ignore if jira issue is not tracking
        return;
      }
      log.info("update JIRA#{}", issue.getKey());
      jis.setCreatedAt(fields.getCreated());
      jis.setUpdatedAt(fields.getUpdated());
      jiraIssueDao.update(jis);
    } else {
      log.info("start tracking JIRA#{}", issue.getKey());
      jis = jiraIssueDao.create(issue.getKey(), false, fields.getUpdated(), fields.getCreated());
    }

    var num = issue.getKey().split("-")[1];
    if (!kbPostBindingDao.exists("solutions", "jira:internal", num)) {
      String kbId = null;
      if (StringUtils.hasText(issue.getFields().getKbLink())) {
        var matcher = Pattern.compile(kbClient.getConf().getBaseUrl() + "/post/solutions/(\\d+)").matcher(issue.getFields().getKbLink());
        kbId = matcher.results().findFirst().map(res -> res.group(1)).orElse(null);
        if (kbId == null) {
          log.info("jira#{} already has kb link {}", issue.getKey(), issue.getFields().getKbLink());
          return;
        }
      }
      Optional<Solution> oSolution = Optional.ofNullable(kbId).map(Long::valueOf).flatMap(kbClient::getSolution).or(() -> kbClient.getSolutionByBackTrackLink(issue.getKey()));
      if (oSolution.isPresent()) {
        // in case that jira is not tracked but kb was already exists.
        Solution solution = kbClient.getSolution(oSolution.get().getId()).orElseThrow();
        log.info("kb for JIRA#{} already exists: {}", issue.getKey(), solution.getId());
        String jiraUrl = jiraClient.getLinkFor(num);
        String jiraLink = solution.getBackTrackLink();
        AtomicBoolean kbMetaUpdated = new AtomicBoolean(false);
        if (StringUtils.hasText(jiraLink)) {
          if (!Objects.equals(jiraLink.trim(), jiraUrl)) {
            log.warn("kb#{} backTrackLink was exist and not equals to {} ({})", solution.getId(), jiraLink, jiraUrl);
            return;
          }
        } else {
          kbMetaUpdated.set(true);
        }
        solution.setBackTrackLink(jiraUrl);
        if (solution.getTags().stream().noneMatch(tag -> tag.getName().startsWith("kbCandidate"))) {
          kbClient.findTag("kbCandidate").ifPresent(tag -> {
            solution.getTags().add(tag);
            kbMetaUpdated.set(true);
          });
        }

        var jiraUpdate = new JiraUpdate()
            .removeLabel("kb-candidate")
            .removeLabel("kb-critical")
            .setKbLink(jiraClient.getConfig(), kbClient.buildLink("solutions", solution.getId()));
        boolean tracking;
        if (solution.getStatus() == 30) {
          jiraUpdate.addLabel("kb-done");
          tracking = false;
        } else {
          jiraUpdate.addLabel("kb-progress");
          tracking = true;
        }
        kbPostStatusDao.create("solution", solution.getId(), solution.getStatus(), tracking);
        kbPostBindingDao.create("solution", solution.getId(), "jira:internal", num);
        jiraClient.update(num, jiraUpdate);
        if (kbMetaUpdated.get()) {
          kbClient.updateSolution(solution, true);
        }
      } else {
        if (kbId != null) {
          log.warn("JIRA#{} bad kb id {};", issue.getKey(), kbId);
          return;
        }
        List<Long> tags = fields.getLabels().stream()
            .map(name -> new LowerCamelCaseStrategy().translate(name))
            .map(kbClient::findTag)
            .flatMap(Optional::stream)
            .map(Meta::getId)
            .collect(Collectors.toList());
        kbClient.findTag("kbCandidate").map(Meta::getId).ifPresent(tags::add);

        Solution material = kbClient.createMaterial(
            fields.getSummary(),
            fields.getReporter().getEmailAddress(),
            jiraClient.getLinkFor(num),
            tags.stream().collect(Collectors.toUnmodifiableSet()).stream().toList());
        kbPostStatusDao.create("solutions", material.getId(), material.getStatus(), true);
        kbPostBindingDao.create("solutions", material.getId(), "jira:internal", num);
        jiraClient.update(num, new JiraUpdate()
            .addLabel("kb-progress")
            .removeLabel("kb-candidate")
            .removeLabel("kb-critical")
            .setKbLink(jiraClient.getConfig(), kbClient.buildLink("solutions", material.getId())));
      }
      jis.setTracking(false);
      jiraIssueDao.update(jis);
    } else {
      log.warn("jira kb biding already exists for jira#{}", issue.getKey());
    }
  }

  public void trackSolution(Solution solution) {
    if (kbPostStatusDao.find("solutions", solution.getId()).isPresent()) {
      log.info("ignore kb#{} for: {}", solution.getId(), "non-tracking solution");
      return;
    }
    solution = kbClient.getSolution(solution.getId()).orElseThrow();
    String num = null;
    Matcher matcher = Pattern.compile(jiraClient.getConfig().getProject() + "-(\\d+)").matcher(solution.getBackTrackLink());
    while (matcher.find()) {
      if (num != null) {
        log.info("ignore kb#{} for: multiple JIRA urls", solution.getId());
        return;
      }
      num = matcher.group(1);
    }
    if (num == null) {
      log.info("ignore kb#{} for: no JIRA key matched", solution.getId());
      return;
    }
    String key = jiraClient.getConfig().getProject() + "-" + num;
    JiraUpdate jiraUpdate = new JiraUpdate().removeLabel("kb-candidate").removeLabel("kb-critical");
    JiraIssue jiraIssue = jiraClient.get(num).orElse(null);
    if (jiraIssue == null) {
      log.info("ignore kb#{} for: JIRA not found", solution.getId());
      return;
    }
    final String kbUrl = kbClient.buildLink("solutions", solution.getId());
    String kbLink = jiraIssue.getFields().getKbLink();
    if (StringUtils.hasText(kbLink)) {
      if (Objects.equals(kbLink, kbUrl)) {
        log.info("ignore kb#{} for: JIRA already had a kb link {}", solution.getId(), kbLink);
        return;
      } else if (Objects.equals(kbLink, kbClient.getConf().getBaseUrl() + "/post/solutions" + solution.getId())) {
        // fix wrong urls
        jiraUpdate.setKbLink(jiraClient.getConfig(), kbUrl);
      }
    } else {
      jiraUpdate.setKbLink(jiraClient.getConfig(), kbUrl);
    }
    boolean tracking;
    if (solution.getStatus() == 30) {
      jiraUpdate.addLabel("kb-done");
      tracking = false;
    } else {
      jiraUpdate.addLabel("kb-progress");
      tracking = true;
    }

    log.info("track kb#{} ~ jira#{}", solution.getId(), jiraIssue.getKey());
    long solutionId = solution.getId();
    int solutionStatus = solution.getStatus();
    kbPostStatusDao.find("solution", solution.getId())
        .ifPresentOrElse(
            kps -> kbPostStatusDao.setTrack("solution", solutionId, tracking),
            () -> kbPostStatusDao.create("solution", solutionId, solutionStatus, tracking));
    if (!Objects.equals(String.valueOf(kbPostBindingDao.findByKb("solution", solutionId, "jira:internal")), num)) {
      kbPostBindingDao.create("solution", solution.getId(), "jira:internal", num);
    }

    jiraIssueDao.findByKey(key)
        .ifPresentOrElse((jis) -> {
          jis.setCreatedAt(jiraIssue.getFields().getCreated());
          jis.setUpdatedAt(jiraIssue.getFields().getUpdated());
          jis.setTracking(false);
          jiraIssueDao.update(jis);
        }, () -> {
          jiraIssueDao.create(key, false, jiraIssue.getFields().getUpdated(), jiraIssue.getFields().getCreated());
        });
    jiraClient.update(num, jiraUpdate);
  }

  public void processSolution(Solution solution) {
    KbPostStatus postStatus = kbPostStatusDao.find("solutions", solution.getId()).orElse(null);
    if (postStatus == null) {
      log.info("ignore message for: {}", "non-tracking solution");
      return;
    }
    Integer num = kbPostBindingDao.findByKb("solutions", postStatus.getKbId(), "jira:internal");
    if (num == null) {
      log.info("ignore message for: {}", "binding jira num not found");
      return;
    }
    if (!postStatus.isTracking()) {
      log.info("ignore message for: {}", "solution is marked not tracking");
    }
    if (solution.getStatus() != 30) {
      log.info("ignore message for: {}", "solution is not published");
    }
    try {
      jiraClient.update(num.toString(), new JiraUpdate()
          .removeLabel("kb-candidate")
          .removeLabel("kb-critical")
          .removeLabel("kb-progress")
          .addLabel("kb-done"));
      kbPostStatusDao.setTrack("solutions", postStatus.getKbId(), false);
      log.info("issue ONCALL-{} marked as kb-done", num);
    } catch (Throwable e) {
      log.error("issue state not synced", e);
    }
  }

}
