// Copyright 2019 clair authors
//
// 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.

package vulnerability

import (
	"database/sql"

	"github.com/coreos/clair/database"
	"github.com/coreos/clair/database/pgsql/feature"
	"github.com/coreos/clair/database/pgsql/util"
	"github.com/lib/pq"
	log "github.com/sirupsen/logrus"
)

const (
	searchNamespacedFeaturesVulnerabilities = `
	SELECT vanf.namespaced_feature_id, v.name, v.description, v.link, 
		v.severity, v.metadata, vaf.fixedin, n.name, n.version_format
	FROM vulnerability_affected_namespaced_feature AS vanf, 
		Vulnerability AS v,
		vulnerability_affected_feature AS vaf,
		namespace AS n
	WHERE vanf.namespaced_feature_id = ANY($1)
		AND vaf.id = vanf.added_by
		AND v.id = vanf.vulnerability_id
		AND n.id = v.namespace_id
		AND v.deleted_at IS NULL`

	lockVulnerabilityAffects = `LOCK vulnerability_affected_namespaced_feature IN SHARE ROW EXCLUSIVE MODE`

	insertVulnerabilityAffectedNamespacedFeature = `
		INSERT INTO vulnerability_affected_namespaced_feature(vulnerability_id, namespaced_feature_id, added_by)
		VALUES ($1, $2, $3)`
)

func queryPersistVulnerabilityAffectedNamespacedFeature(count int) string {
	return util.QueryPersist(count, "vulnerability_affected_namespaced_feature",
		"vulnerability_affected_namesp_vulnerability_id_namespaced_f_key",
		"vulnerability_id",
		"namespaced_feature_id",
		"added_by")
}

// FindAffectedNamespacedFeatures retrieves vulnerabilities associated with the
// feature.
func FindAffectedNamespacedFeatures(tx *sql.Tx, features []database.NamespacedFeature) ([]database.NullableAffectedNamespacedFeature, error) {
	if len(features) == 0 {
		return nil, nil
	}

	vulnerableFeatures := make([]database.NullableAffectedNamespacedFeature, len(features))
	featureIDs, err := feature.FindNamespacedFeatureIDs(tx, features)
	if err != nil {
		return nil, err
	}

	for i, id := range featureIDs {
		if id.Valid {
			vulnerableFeatures[i].Valid = true
			vulnerableFeatures[i].NamespacedFeature = features[i]
		}
	}

	rows, err := tx.Query(searchNamespacedFeaturesVulnerabilities, pq.Array(featureIDs))
	if err != nil {
		return nil, util.HandleError("searchNamespacedFeaturesVulnerabilities", err)
	}
	defer rows.Close()

	for rows.Next() {
		var (
			featureID int64
			vuln      database.VulnerabilityWithFixedIn
		)

		err := rows.Scan(&featureID,
			&vuln.Name,
			&vuln.Description,
			&vuln.Link,
			&vuln.Severity,
			&vuln.Metadata,
			&vuln.FixedInVersion,
			&vuln.Namespace.Name,
			&vuln.Namespace.VersionFormat,
		)

		if err != nil {
			return nil, util.HandleError("searchNamespacedFeaturesVulnerabilities", err)
		}

		for i, id := range featureIDs {
			if id.Valid && id.Int64 == featureID {
				vulnerableFeatures[i].AffectedNamespacedFeature.AffectedBy = append(vulnerableFeatures[i].AffectedNamespacedFeature.AffectedBy, vuln)
			}
		}
	}

	return vulnerableFeatures, nil
}

func CacheAffectedNamespacedFeatures(tx *sql.Tx, features []database.NamespacedFeature) error {
	if len(features) == 0 {
		return nil
	}

	_, err := tx.Exec(lockVulnerabilityAffects)
	if err != nil {
		return util.HandleError("lockVulnerabilityAffects", err)
	}

	cache, err := SearchAffectingVulnerabilities(tx, features)

	keys := make([]interface{}, 0, len(cache)*3)
	for _, c := range cache {
		keys = append(keys, c.vulnID, c.nsFeatureID, c.vulnAffectingID)
	}

	if len(cache) == 0 {
		return nil
	}

	affected, err := tx.Exec(queryPersistVulnerabilityAffectedNamespacedFeature(len(cache)), keys...)
	if err != nil {
		return util.HandleError("persistVulnerabilityAffectedNamespacedFeature", err)
	}
	if count, err := affected.RowsAffected(); err != nil {
		log.Debugf("Cached %d features in vulnerability_affected_namespaced_feature", count)
	}
	return nil
}
