/*
 * This file is part of Dependency-Track.
 *
 * 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
 *
 *   http://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.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) Steve Springett. All Rights Reserved.
 */
package org.dependencytrack.model;

import alpine.common.validation.RegexSequence;
import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.nimbusds.jose.util.Pair;

import javax.jdo.annotations.Column;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Index;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import javax.jdo.annotations.Unique;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Model for tracking alias for vulnerabilities.
 *
 * @author Steve Springett
 * @since 4.6.0
 */
@PersistenceCapable
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VulnerabilityAlias implements Serializable {

    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.NATIVE)
    @JsonIgnore
    private long id;

    @Persistent
    @Column(name = "INTERNAL_ID")
    @Index(name = "VULNERABILITYALIAS_INTERNAL_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The internalId field may only contain printable characters")
    private String internalId;

    @Persistent
    @Column(name = "CVE_ID")
    @Index(name = "VULNERABILITYALIAS_CVE_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The cveId field may only contain printable characters")
    private String cveId;

    @Persistent
    @Column(name = "GHSA_ID")
    @Index(name = "VULNERABILITYALIAS_GHSA_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The ghsaId field may only contain printable characters")
    private String ghsaId;

    @Persistent
    @Column(name = "SONATYPE_ID")
    @Index(name = "VULNERABILITYALIAS_SONATYPE_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The sonatypeId field may only contain printable characters")
    private String sonatypeId;

    @Persistent
    @Column(name = "OSV_ID")
    @Index(name = "VULNERABILITYALIAS_OSV_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The osvId field may only contain printable characters")
    private String osvId;

    @Persistent
    @Column(name = "SNYK_ID")
    @Index(name = "VULNERABILITYALIAS_SNYK_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The snykId field may only contain printable characters")
    private String snykId;

    @Persistent
    @Column(name = "GSD_ID")
    @Index(name = "VULNERABILITYALIAS_GSD_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The gsdId field may only contain printable characters")
    private String gsdId;

    @Persistent
    @Column(name = "VULNDB_ID")
    @Index(name = "VULNERABILITYALIAS_VULNDB_ID_IDX")
    @JsonDeserialize(using = TrimmedStringDeserializer.class)
    @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS_PLUS, message = "The vulnDbId field may only contain printable characters")
    private String vulnDbId;

    @Persistent(customValueStrategy = "uuid")
    @Unique(name = "VULNERABILITYALIAS_UUID_IDX")
    @Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "false")
    @NotNull
    private UUID uuid;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getInternalId() {
        return internalId;
    }

    public void setInternalId(String internalId) {
        this.internalId = internalId;
    }

    public String getCveId() {
        return cveId;
    }

    public void setCveId(String cveId) {
        this.cveId = cveId;
    }

    public String getGhsaId() {
        return ghsaId;
    }

    public void setGhsaId(String ghsaId) {
        this.ghsaId = ghsaId;
    }

    public String getSonatypeId() {
        return sonatypeId;
    }

    public void setSonatypeId(String sonatypeId) {
        this.sonatypeId = sonatypeId;
    }

    public String getOsvId() {
        return osvId;
    }

    public void setOsvId(String osvId) {
        this.osvId = osvId;
    }

    public String getSnykId() { return snykId; }

    public void setSnykId(String snykId) { this.snykId = snykId; }

    public String getGsdId() {
        return gsdId;
    }

    public void setGsdId(String gsdId) {
        this.gsdId = gsdId;
    }

    public String getVulnDbId() {
        return vulnDbId;
    }

    public void setVulnDbId(String vulnDbId) {
        this.vulnDbId = vulnDbId;
    }

    public UUID getUuid() {
        return uuid;
    }

    public void setUuid(UUID uuid) {
        this.uuid = uuid;
    }

    private String getBySource(final Vulnerability.Source source) {
        return switch (source) {
            case GITHUB -> getGhsaId();
            case INTERNAL -> getInternalId();
            case NVD -> getCveId();
            case OSSINDEX -> getSonatypeId();
            case OSV -> getOsvId();
            case SNYK -> getSnykId();
            case VULNDB -> getVulnDbId();
            default -> null;
        };
    }

    @JsonIgnore
    public Map<Vulnerability.Source, String> getAllBySource() {
        return Arrays.stream(Vulnerability.Source.values())
                .map(source -> Pair.of(source, getBySource(source)))
                .filter(pair -> pair.getRight() != null)
                .collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
    }

    public void copyFieldsFrom(VulnerabilityAlias other) {
        // Check if each of the fields are null. We don't want to overwrite where only partial information is known, with nulls.
        cveId = firstNonNull(other.cveId, cveId);
        sonatypeId = firstNonNull(other.sonatypeId, sonatypeId);
        ghsaId = firstNonNull(other.ghsaId, ghsaId);
        osvId = firstNonNull(other.osvId, osvId);
        snykId = firstNonNull(other.snykId, snykId);
        gsdId = firstNonNull(other.gsdId, gsdId);
        vulnDbId = firstNonNull(other.vulnDbId, vulnDbId);
        internalId = firstNonNull(other.internalId, internalId);
    }

    private static String firstNonNull(String first, String second) {
        return first != null ? first : second;
    }

    /**
     * Compute how many vulnerability identifiers of this {@link VulnerabilityAlias}
     * match with those of a given other {@link VulnerabilityAlias}.
     * <p>
     * Identifiers are considered to be matches when they are equal but not {@code null}.
     *
     * @param other The {@link VulnerabilityAlias} to compute matches with
     * @return Number of matching identifiers
     */
    public int computeMatches(final VulnerabilityAlias other) {
        var matches = 0;

        if (this.getCveId() != null && this.getCveId().equals(other.getCveId())) {
            matches++;
        }
        if (this.getGhsaId() != null && this.getGhsaId().equals(other.getGhsaId())) {
            matches++;
        }
        if (this.getGsdId() != null && this.getGsdId().equals(other.getGsdId())) {
            matches++;
        }
        if (this.getOsvId() != null && this.getOsvId().equals(other.getOsvId())) {
            matches++;
        }
        if (this.getSnykId() != null && this.getSnykId().equals(other.getSnykId())) {
            matches++;
        }
        if (this.getSonatypeId() != null && this.getSonatypeId().equals(other.getSonatypeId())) {
            matches++;
        }
        if (this.getVulnDbId() != null && this.getVulnDbId().equals(other.getVulnDbId())) {
            matches++;
        }

        return matches;
    }

    @Override
    public String toString() {
        return "VulnerabilityAlias{" +
                "id=" + id +
                ", internalId='" + internalId + '\'' +
                ", cveId='" + cveId + '\'' +
                ", ghsaId='" + ghsaId + '\'' +
                ", sonatypeId='" + sonatypeId + '\'' +
                ", osvId='" + osvId + '\'' +
                ", snykId='" + snykId + '\'' +
                ", gsdId='" + gsdId + '\'' +
                ", vulnDbId='" + vulnDbId + '\'' +
                ", uuid=" + uuid +
                '}';
    }

}
