/* Vuls - Vulnerability Scanner
Copyright (C) 2016  Future Architect, Inc. Japan.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package models

import (
	"fmt"
	"sort"
	"time"

	"github.com/future-architect/vuls/config"
	"github.com/future-architect/vuls/cveapi"
	cve "github.com/kotakanbe/go-cve-dictionary/models"
)

// ScanHistory is the history of Scanning.
type ScanHistory struct {
	ScanResults ScanResults
}

// ScanResults is slice of ScanResult.
type ScanResults []ScanResult

// Len implement Sort Interface
func (s ScanResults) Len() int {
	return len(s)
}

// Swap implement Sort Interface
func (s ScanResults) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

// Less implement Sort Interface
func (s ScanResults) Less(i, j int) bool {
	if s[i].ServerName == s[j].ServerName {
		return s[i].Container.ContainerID < s[i].Container.ContainerID
	}
	return s[i].ServerName < s[j].ServerName
}

// ScanResult has the result of scanned CVE information.
type ScanResult struct {
	ScannedAt time.Time

	Lang       string
	ServerName string // TOML Section key
	Family     string
	Release    string
	Container  Container
	Platform   Platform

	// Scanned Vulns via SSH + CPE Vulns
	ScannedCves []VulnInfo

	KnownCves   []CveInfo
	UnknownCves []CveInfo
	IgnoredCves []CveInfo

	Packages PackageInfoList

	Errors   []string
	Optional [][]interface{}
}

// FillCveDetail fetches CVE detailed information from
// CVE Database, and then set to fields.
func (r ScanResult) FillCveDetail() (ScanResult, error) {
	set := map[string]VulnInfo{}
	var cveIDs []string
	for _, v := range r.ScannedCves {
		set[v.CveID] = v
		cveIDs = append(cveIDs, v.CveID)
	}

	ds, err := cveapi.CveClient.FetchCveDetails(cveIDs)
	if err != nil {
		return r, err
	}

	known, unknown, ignored := CveInfos{}, CveInfos{}, CveInfos{}
	for _, d := range ds {
		cinfo := CveInfo{
			CveDetail: d,
			VulnInfo:  set[d.CveID],
		}
		cinfo.NilSliceToEmpty()

		// ignored
		found := false
		for _, icve := range config.Conf.Servers[r.ServerName].IgnoreCves {
			if icve == d.CveID {
				ignored = append(ignored, cinfo)
				found = true
				break
			}
		}
		if found {
			continue
		}

		// unknown
		if d.CvssScore(config.Conf.Lang) <= 0 {
			unknown = append(unknown, cinfo)
			continue
		}

		// known
		known = append(known, cinfo)
	}
	sort.Sort(known)
	sort.Sort(unknown)
	sort.Sort(ignored)
	r.KnownCves = known
	r.UnknownCves = unknown
	r.IgnoredCves = ignored
	return r, nil
}

// FilterByCvssOver is filter function.
func (r ScanResult) FilterByCvssOver() ScanResult {
	cveInfos := []CveInfo{}
	for _, cveInfo := range r.KnownCves {
		if config.Conf.CvssScoreOver < cveInfo.CveDetail.CvssScore(config.Conf.Lang) {
			cveInfos = append(cveInfos, cveInfo)
		}
	}
	r.KnownCves = cveInfos
	return r
}

// ReportFileName returns the filename on localhost without extention
func (r ScanResult) ReportFileName() (name string) {
	if len(r.Container.ContainerID) == 0 {
		return fmt.Sprintf("%s", r.ServerName)
	}
	return fmt.Sprintf("%s@%s", r.Container.Name, r.ServerName)
}

// ReportKeyName returns the name of key on S3, Azure-Blob without extention
func (r ScanResult) ReportKeyName() (name string) {
	timestr := r.ScannedAt.Format(time.RFC3339)
	if len(r.Container.ContainerID) == 0 {
		return fmt.Sprintf("%s/%s", timestr, r.ServerName)
	}
	return fmt.Sprintf("%s/%s@%s", timestr, r.Container.Name, r.ServerName)
}

// ServerInfo returns server name one line
func (r ScanResult) ServerInfo() string {
	if len(r.Container.ContainerID) == 0 {
		return fmt.Sprintf("%s (%s%s)",
			r.ServerName, r.Family, r.Release)
	}
	return fmt.Sprintf(
		"%s / %s (%s%s) on %s",
		r.Container.Name,
		r.Container.ContainerID,
		r.Family,
		r.Release,
		r.ServerName,
	)
}

// ServerInfoTui returns server infromation for TUI sidebar
func (r ScanResult) ServerInfoTui() string {
	if len(r.Container.ContainerID) == 0 {
		return fmt.Sprintf("%s (%s%s)",
			r.ServerName, r.Family, r.Release)
	}
	return fmt.Sprintf(
		"|-- %s (%s%s)",
		r.Container.Name,
		r.Family,
		r.Release,
		//  r.Container.ContainerID,
	)
}

// FormatServerName returns server and container name
func (r ScanResult) FormatServerName() string {
	if len(r.Container.ContainerID) == 0 {
		return r.ServerName
	}
	return fmt.Sprintf("%s@%s",
		r.Container.Name, r.ServerName)
}

// CveSummary summarize the number of CVEs group by CVSSv2 Severity
func (r ScanResult) CveSummary() string {
	var high, medium, low, unknown int
	cves := append(r.KnownCves, r.UnknownCves...)
	for _, cveInfo := range cves {
		score := cveInfo.CveDetail.CvssScore(config.Conf.Lang)
		switch {
		case 7.0 <= score:
			high++
		case 4.0 <= score:
			medium++
		case 0 < score:
			low++
		default:
			unknown++
		}
	}

	if config.Conf.IgnoreUnscoredCves {
		return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d)",
			high+medium+low, high, medium, low)
	}
	return fmt.Sprintf("Total: %d (High:%d Medium:%d Low:%d ?:%d)",
		high+medium+low+unknown, high, medium, low, unknown)
}

// AllCves returns Known and Unknown CVEs
func (r ScanResult) AllCves() []CveInfo {
	return append(r.KnownCves, r.UnknownCves...)
}

// NWLink has network link information.
type NWLink struct {
	IPAddress string
	Netmask   string
	DevName   string
	LinkState string
}

// Confidence is a ranking how confident the CVE-ID was deteted correctly
// Score: 0 - 100
type Confidence struct {
	Score           int
	DetectionMethod string
}

func (c Confidence) String() string {
	return fmt.Sprintf("%d / %s", c.Score, c.DetectionMethod)
}

// CpeNameMatch is a ranking how confident the CVE-ID was deteted correctly
var CpeNameMatch = Confidence{100, "CpeNameMatch"}

// YumUpdateSecurityMatch is a ranking how confident the CVE-ID was deteted correctly
var YumUpdateSecurityMatch = Confidence{100, "YumUpdateSecurityMatch"}

// PkgAuditMatch is a ranking how confident the CVE-ID was deteted correctly
var PkgAuditMatch = Confidence{100, "PkgAuditMatch"}

// ChangelogExactMatch is a ranking how confident the CVE-ID was deteted correctly
var ChangelogExactMatch = Confidence{95, "ChangelogExactMatch"}

// ChangelogLenientMatch is a ranking how confident the CVE-ID was deteted correctly
var ChangelogLenientMatch = Confidence{50, "ChangelogLenientMatch"}

// VulnInfos is VulnInfo list, getter/setter, sortable methods.
type VulnInfos []VulnInfo

// VulnInfo holds a vulnerability information and unsecure packages
type VulnInfo struct {
	CveID            string
	Confidence       Confidence
	Packages         PackageInfoList
	DistroAdvisories []DistroAdvisory // for Aamazon, RHEL, FreeBSD
	CpeNames         []string
}

// NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON
func (v *VulnInfo) NilSliceToEmpty() {
	if v.CpeNames == nil {
		v.CpeNames = []string{}
	}
	if v.DistroAdvisories == nil {
		v.DistroAdvisories = []DistroAdvisory{}
	}
}

// FindByCveID find by CVEID
func (s VulnInfos) FindByCveID(cveID string) (VulnInfo, bool) {
	for _, p := range s {
		if cveID == p.CveID {
			return p, true
		}
	}
	return VulnInfo{CveID: cveID}, false
}

// immutable
func (s VulnInfos) set(cveID string, v VulnInfo) VulnInfos {
	for i, p := range s {
		if cveID == p.CveID {
			s[i] = v
			return s
		}
	}
	return append(s, v)
}

// Len implement Sort Interface
func (s VulnInfos) Len() int {
	return len(s)
}

// Swap implement Sort Interface
func (s VulnInfos) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

// Less implement Sort Interface
func (s VulnInfos) Less(i, j int) bool {
	return s[i].CveID < s[j].CveID
}

// CveInfos is for sorting
type CveInfos []CveInfo

func (c CveInfos) Len() int {
	return len(c)
}

func (c CveInfos) Swap(i, j int) {
	c[i], c[j] = c[j], c[i]
}

func (c CveInfos) Less(i, j int) bool {
	lang := config.Conf.Lang
	if c[i].CveDetail.CvssScore(lang) == c[j].CveDetail.CvssScore(lang) {
		return c[i].CveDetail.CveID < c[j].CveDetail.CveID
	}
	return c[j].CveDetail.CvssScore(lang) < c[i].CveDetail.CvssScore(lang)
}

// CveInfo has Cve Information.
type CveInfo struct {
	CveDetail cve.CveDetail
	VulnInfo
}

// NilSliceToEmpty set nil slice fields to empty slice to avoid null in JSON
func (c *CveInfo) NilSliceToEmpty() {
	if c.CveDetail.Nvd.Cpes == nil {
		c.CveDetail.Nvd.Cpes = []cve.Cpe{}
	}
	if c.CveDetail.Jvn.Cpes == nil {
		c.CveDetail.Jvn.Cpes = []cve.Cpe{}
	}
	if c.CveDetail.Nvd.References == nil {
		c.CveDetail.Nvd.References = []cve.Reference{}
	}
	if c.CveDetail.Jvn.References == nil {
		c.CveDetail.Jvn.References = []cve.Reference{}
	}
}

// PackageInfoList is slice of PackageInfo
type PackageInfoList []PackageInfo

// Exists returns true if exists the name
func (ps PackageInfoList) Exists(name string) bool {
	for _, p := range ps {
		if p.Name == name {
			return true
		}
	}
	return false
}

// UniqByName be uniq by name.
func (ps PackageInfoList) UniqByName() (distincted PackageInfoList) {
	set := make(map[string]PackageInfo)
	for _, p := range ps {
		set[p.Name] = p
	}
	//sort by key
	keys := []string{}
	for key := range set {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		distincted = append(distincted, set[key])
	}
	return
}

// FindByName search PackageInfo by name
func (ps PackageInfoList) FindByName(name string) (result PackageInfo, found bool) {
	for _, p := range ps {
		if p.Name == name {
			return p, true
		}
	}
	return PackageInfo{}, false
}

// MergeNewVersion merges candidate version information to the receiver struct
func (ps PackageInfoList) MergeNewVersion(as PackageInfoList) {
	for _, a := range as {
		for i, p := range ps {
			if p.Name == a.Name {
				ps[i].NewVersion = a.NewVersion
				ps[i].NewRelease = a.NewRelease
			}
		}
	}
}

func (ps PackageInfoList) countUpdatablePacks() int {
	count := 0
	for _, p := range ps {
		if len(p.NewVersion) != 0 {
			count++
		}
	}
	return count
}

// ToUpdatablePacksSummary returns a summary of updatable packages
func (ps PackageInfoList) ToUpdatablePacksSummary() string {
	return fmt.Sprintf("%d updatable packages",
		ps.countUpdatablePacks())
}

// Find search PackageInfo by name-version-release
//  func (ps PackageInfoList) find(nameVersionRelease string) (PackageInfo, bool) {
//      for _, p := range ps {
//          joined := p.Name
//          if 0 < len(p.Version) {
//              joined = fmt.Sprintf("%s-%s", joined, p.Version)
//          }
//          if 0 < len(p.Release) {
//              joined = fmt.Sprintf("%s-%s", joined, p.Release)
//          }
//          if joined == nameVersionRelease {
//              return p, true
//          }
//      }
//      return PackageInfo{}, false
//  }

// PackageInfosByName implements sort.Interface for []PackageInfo based on
// the Name field.
type PackageInfosByName []PackageInfo

func (a PackageInfosByName) Len() int           { return len(a) }
func (a PackageInfosByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a PackageInfosByName) Less(i, j int) bool { return a[i].Name < a[j].Name }

// PackageInfo has installed packages.
type PackageInfo struct {
	Name       string
	Version    string
	Release    string
	NewVersion string
	NewRelease string
	Repository string
}

// ToStringCurrentVersion returns package name-version-release
func (p PackageInfo) ToStringCurrentVersion() string {
	str := p.Name
	if 0 < len(p.Version) {
		str = fmt.Sprintf("%s-%s", str, p.Version)
	}
	if 0 < len(p.Release) {
		str = fmt.Sprintf("%s-%s", str, p.Release)
	}
	return str
}

// ToStringNewVersion returns package name-version-release
func (p PackageInfo) ToStringNewVersion() string {
	str := p.Name
	if 0 < len(p.NewVersion) {
		str = fmt.Sprintf("%s-%s", str, p.NewVersion)
	}
	if 0 < len(p.NewRelease) {
		str = fmt.Sprintf("%s-%s", str, p.NewRelease)
	}
	return str
}

// DistroAdvisory has Amazon Linux, RHEL, FreeBSD Security Advisory information.
type DistroAdvisory struct {
	AdvisoryID string
	Severity   string
	Issued     time.Time
	Updated    time.Time
}

// Container has Container information
type Container struct {
	ContainerID string
	Name        string
	Image       string
}

// Platform has platform information
type Platform struct {
	Name       string // aws or azure or gcp or other...
	InstanceID string
}
