package json

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	version "github.com/hashicorp/go-version"
	"github.com/k0kubun/pp"
	"github.com/kotakanbe/go-cve-dictionary/fetcher"
	"github.com/kotakanbe/go-cve-dictionary/log"
	"github.com/kotakanbe/go-cve-dictionary/models"
)

// FetchConvert Fetch CVE vulnerability information from NVD
func FetchConvert(metas []models.FeedMeta) (cves []models.CveDetail, err error) {
	reqs := []fetcher.FetchRequest{}
	for _, meta := range metas {
		reqs = append(reqs, fetcher.FetchRequest{
			URL:  meta.URL,
			GZIP: true,
		})
	}

	results, err := fetcher.FetchFeedFiles(reqs)
	if err != nil {
		return nil,
			fmt.Errorf("Failed to fetch. err: %s", err)
	}

	for _, res := range results {
		nvd := NvdJSON{}
		if err = json.Unmarshal(res.Body, &nvd); err != nil {
			return nil, fmt.Errorf(
				"Failed to unmarshal. url: %s, err: %s",
				res.URL, err)
		}
		for _, item := range nvd.CveItems {
			cve, err := convertToModel(&item)
			if err != nil {
				return nil, fmt.Errorf("Failed to convert to model. cve: %s, err: %s",
					item.Cve.CveDataMeta.ID, err)
			}
			cves = append(cves, *cve)
		}
	}
	return
}

// NvdJSON is a struct of NVD JSON
// https://scap.nist.gov/schema/nvd/feed/0.1/nvd_cve_feed_json_0.1_beta.schema
type NvdJSON struct {
	CveDataType         string    `json:"CVE_data_type"`
	CveDataFormat       string    `json:"CVE_data_format"`
	CveDataVersion      string    `json:"CVE_data_version"`
	CveDataNumberOfCVEs string    `json:"CVE_data_numberOfCVEs"`
	CveDataTimestamp    string    `json:"CVE_data_timestamp"`
	CveItems            []CveItem `json:"CVE_Items"`
}

// CveItem is a struct of NvdJSON>CveItems
type CveItem struct {
	Cve struct {
		DataType    string `json:"data_type"`
		DataFormat  string `json:"data_format"`
		DataVersion string `json:"data_version"`
		CveDataMeta struct {
			ID       string `json:"ID"`
			ASSIGNER string `json:"ASSIGNER"`
		} `json:"CVE_data_meta"`
		Affects struct {
			Vendor struct {
				VendorData []struct {
					VendorName string `json:"vendor_name"`
					Product    struct {
						ProductData []struct {
							ProductName string `json:"product_name"`
							Version     struct {
								VersionData []struct {
									VersionValue string `json:"version_value"`
								} `json:"version_data"`
							} `json:"version"`
						} `json:"product_data"`
					} `json:"product"`
				} `json:"vendor_data"`
			} `json:"vendor"`
		} `json:"affects"`
		Problemtype struct {
			ProblemtypeData []struct {
				Description []struct {
					Lang  string `json:"lang"`
					Value string `json:"value"`
				} `json:"description"`
			} `json:"problemtype_data"`
		} `json:"problemtype"`
		References struct {
			ReferenceData []struct {
				URL string `json:"url"`
			} `json:"reference_data"`
		} `json:"references"`
		Description struct {
			DescriptionData []struct {
				Lang  string `json:"lang"`
				Value string `json:"value"`
			} `json:"description_data"`
		} `json:"description"`
	} `json:"cve"`
	Configurations struct {
		CveDataVersion string `json:"CVE_data_version"`
		Nodes          []struct {
			Operator string `json:"operator"`
			Negate   bool   `json:"negate"`
			Cpes     []struct {
				Vulnerable            bool   `json:"vulnerable"`
				Cpe23URI              string `json:"cpe23Uri"`
				VersionStartExcluding string `json:"versionStartExcluding"`
				VersionStartIncluding string `json:"versionStartIncluding"`
				VersionEndExcluding   string `json:"versionEndExcluding"`
				VersionEndIncluding   string `json:"versionEndIncluding"`
			} `json:"cpe_match"`
			Children []struct {
				Operator string `json:"operator"`
				Cpes     []struct {
					Vulnerable            bool   `json:"vulnerable"`
					Cpe23URI              string `json:"cpe23Uri"`
					VersionStartExcluding string `json:"versionStartExcluding"`
					VersionStartIncluding string `json:"versionStartIncluding"`
					VersionEndExcluding   string `json:"versionEndExcluding"`
					VersionEndIncluding   string `json:"versionEndIncluding"`
				} `json:"cpe_match"`
			} `json:"children,omitempty"`
		} `json:"nodes"`
	} `json:"configurations"`
	Impact struct {
		BaseMetricV3 struct {
			CvssV3 struct {
				Version               string  `json:"version"`
				VectorString          string  `json:"vectorString"`
				AttackVector          string  `json:"attackVector"`
				AttackComplexity      string  `json:"attackComplexity"`
				PrivilegesRequired    string  `json:"privilegesRequired"`
				UserInteraction       string  `json:"userInteraction"`
				Scope                 string  `json:"scope"`
				ConfidentialityImpact string  `json:"confidentialityImpact"`
				IntegrityImpact       string  `json:"integrityImpact"`
				AvailabilityImpact    string  `json:"availabilityImpact"`
				BaseScore             float64 `json:"baseScore"`
				BaseSeverity          string  `json:"baseSeverity"`
			} `json:"cvssV3"`
			ExploitabilityScore float64 `json:"exploitabilityScore"`
			ImpactScore         float64 `json:"impactScore"`
		} `json:"baseMetricV3"`
		BaseMetricV2 struct {
			CvssV2 struct {
				Version               string  `json:"version"`
				VectorString          string  `json:"vectorString"`
				AccessVector          string  `json:"accessVector"`
				AccessComplexity      string  `json:"accessComplexity"`
				Authentication        string  `json:"authentication"`
				ConfidentialityImpact string  `json:"confidentialityImpact"`
				IntegrityImpact       string  `json:"integrityImpact"`
				AvailabilityImpact    string  `json:"availabilityImpact"`
				BaseScore             float64 `json:"baseScore"`
			} `json:"cvssV2"`
			Severity                string  `json:"severity"`
			ExploitabilityScore     float64 `json:"exploitabilityScore"`
			ImpactScore             float64 `json:"impactScore"`
			ObtainAllPrivilege      bool    `json:"obtainAllPrivilege"`
			ObtainUserPrivilege     bool    `json:"obtainUserPrivilege"`
			ObtainOtherPrivilege    bool    `json:"obtainOtherPrivilege"`
			UserInteractionRequired bool    `json:"userInteractionRequired"`
		} `json:"baseMetricV2"`
	} `json:"impact"`
	PublishedDate    string `json:"publishedDate"`
	LastModifiedDate string `json:"lastModifiedDate"`
}

// convertToModel converts Nvd JSON to model structure.
func convertToModel(item *CveItem) (*models.CveDetail, error) {
	//References
	refs := []models.Reference{}
	for _, r := range item.Cve.References.ReferenceData {
		ref := models.Reference{
			Link: r.URL,
		}
		refs = append(refs, ref)
	}

	// Cwes
	cwes := []models.Cwe{}
	for _, data := range item.Cve.Problemtype.ProblemtypeData {
		for _, desc := range data.Description {
			cwes = append(cwes, models.Cwe{
				CweID: desc.Value,
			})
		}
	}

	// Affects
	affects := []models.Affect{}
	for _, vendor := range item.Cve.Affects.Vendor.VendorData {
		for _, prod := range vendor.Product.ProductData {
			for _, version := range prod.Version.VersionData {
				affects = append(affects, models.Affect{
					Vendor:  vendor.VendorName,
					Product: prod.ProductName,
					Version: version.VersionValue,
				})
			}
		}
	}

	// Traverse Cpe, EnvCpe
	cpes := []models.Cpe{}
	for _, node := range item.Configurations.Nodes {
		if node.Negate {
			continue
		}

		nodeCpes := []models.Cpe{}
		for _, cpe := range node.Cpes {
			if !cpe.Vulnerable {
				// CVE-2017-14492 and CVE-2017-8581 has a cpe that has vulenrable:false.
				// But these vulnerable: false cpe is also vulnerable...
				// So, ignore the vulerable flag of this layer(under nodes>cpe)
			}
			cpeBase, err := fetcher.ParseCpeURI(cpe.Cpe23URI)
			if err != nil {
				// logging only
				log.Infof("Failed to parse CpeURI %s: %s", cpe.Cpe23URI, err)
				continue
			}
			cpeBase.VersionStartExcluding = cpe.VersionStartExcluding
			cpeBase.VersionStartIncluding = cpe.VersionStartIncluding
			cpeBase.VersionEndExcluding = cpe.VersionEndExcluding
			cpeBase.VersionEndIncluding = cpe.VersionEndIncluding
			nodeCpes = append(nodeCpes, models.Cpe{
				CpeBase: *cpeBase,
			})
			if !checkIfVersionParsable(cpeBase) {
				return nil, fmt.Errorf(
					"Version parse err. Please add a issue on [GitHub](https://github.com/kotakanbe/go-cve-dictionary/issues/new). Title: %s, Content:%s",
					item.Cve.CveDataMeta.ID,
					pp.Sprintf("%v", *item),
				)
			}
		}
		for _, child := range node.Children {
			for _, cpe := range child.Cpes {
				if cpe.Vulnerable {
					cpeBase, err := fetcher.ParseCpeURI(cpe.Cpe23URI)
					if err != nil {
						return nil, err
					}
					cpeBase.VersionStartExcluding = cpe.VersionStartExcluding
					cpeBase.VersionStartIncluding = cpe.VersionStartIncluding
					cpeBase.VersionEndExcluding = cpe.VersionEndExcluding
					cpeBase.VersionEndIncluding = cpe.VersionEndIncluding
					nodeCpes = append(nodeCpes, models.Cpe{
						CpeBase: *cpeBase,
					})
					if !checkIfVersionParsable(cpeBase) {
						return nil, fmt.Errorf(
							"Version parse err. Please add a issue on [GitHub](https://github.com/kotakanbe/go-cve-dictionary/issues/new). Title: %s, Content:%s",
							item.Cve.CveDataMeta.ID,
							pp.Sprintf("%v", *item),
						)
					}
				} else {
					if node.Operator == "AND" {
						for i, c := range nodeCpes {
							cpeBase, err := fetcher.ParseCpeURI(cpe.Cpe23URI)
							if err != nil {
								return nil, err
							}
							cpeBase.VersionStartExcluding = cpe.VersionStartExcluding
							cpeBase.VersionStartIncluding = cpe.VersionStartIncluding
							cpeBase.VersionEndExcluding = cpe.VersionEndExcluding
							cpeBase.VersionEndIncluding = cpe.VersionEndIncluding
							nodeCpes[i].EnvCpes = append(c.EnvCpes, models.EnvCpe{
								CpeBase: *cpeBase,
							})

							if !checkIfVersionParsable(cpeBase) {
								return nil, fmt.Errorf(
									"Please add a issue on [GitHub](https://github.com/kotakanbe/go-cve-dictionary/issues/new). Title: Version parse err: %s, Content:%s",
									item.Cve.CveDataMeta.ID,
									pp.Sprintf("%v", *item),
								)
							}
						}
					}
				}
			}
		}
		cpes = append(cpes, nodeCpes...)
	}

	// Description
	descs := []models.Description{}
	for _, desc := range item.Cve.Description.DescriptionData {
		descs = append(descs, models.Description{
			Lang:  desc.Lang,
			Value: desc.Value,
		})
	}

	publish, err := parseNvdJSONTime(item.PublishedDate)
	if err != nil {
		return nil, err
	}
	modified, err := parseNvdJSONTime(item.LastModifiedDate)
	if err != nil {
		return nil, err
	}
	c2 := item.Impact.BaseMetricV2
	c3 := item.Impact.BaseMetricV3

	return &models.CveDetail{
		CveID: item.Cve.CveDataMeta.ID,
		NvdJSON: &models.NvdJSON{
			CveID:        item.Cve.CveDataMeta.ID,
			Descriptions: descs,
			Cvss2: models.Cvss2Extra{
				Cvss2: models.Cvss2{
					VectorString:          c2.CvssV2.VectorString,
					AccessVector:          c2.CvssV2.AccessVector,
					AccessComplexity:      c2.CvssV2.AccessComplexity,
					Authentication:        c2.CvssV2.Authentication,
					ConfidentialityImpact: c2.CvssV2.ConfidentialityImpact,
					IntegrityImpact:       c2.CvssV2.IntegrityImpact,
					AvailabilityImpact:    c2.CvssV2.AvailabilityImpact,
					BaseScore:             c2.CvssV2.BaseScore,
					Severity:              c2.Severity,
				},
				ExploitabilityScore:     c2.ExploitabilityScore,
				ImpactScore:             c2.ImpactScore,
				ObtainAllPrivilege:      c2.ObtainAllPrivilege,
				ObtainUserPrivilege:     c2.ObtainUserPrivilege,
				ObtainOtherPrivilege:    c2.ObtainOtherPrivilege,
				UserInteractionRequired: c2.UserInteractionRequired,
			},
			Cvss3: models.Cvss3{
				VectorString:          c3.CvssV3.VectorString,
				AttackVector:          c3.CvssV3.AttackVector,
				AttackComplexity:      c3.CvssV3.AttackComplexity,
				PrivilegesRequired:    c3.CvssV3.PrivilegesRequired,
				UserInteraction:       c3.CvssV3.UserInteraction,
				Scope:                 c3.CvssV3.Scope,
				ConfidentialityImpact: c3.CvssV3.ConfidentialityImpact,
				IntegrityImpact:       c3.CvssV3.IntegrityImpact,
				AvailabilityImpact:    c3.CvssV3.AvailabilityImpact,
				BaseScore:             c3.CvssV3.BaseScore,
				BaseSeverity:          c3.CvssV3.BaseSeverity,
				ExploitabilityScore:   c3.ExploitabilityScore,
				ImpactScore:           c3.ImpactScore,
			},
			Cwes:             cwes,
			Cpes:             cpes,
			References:       refs,
			Affects:          affects,
			PublishedDate:    publish,
			LastModifiedDate: modified,
		},
	}, nil
}

func checkIfVersionParsable(cpeBase *models.CpeBase) bool {
	if cpeBase.Version != "ANY" && cpeBase.Version != "NA" {
		vers := []string{cpeBase.VersionStartExcluding,
			cpeBase.VersionStartIncluding,
			cpeBase.VersionEndIncluding,
			cpeBase.VersionEndExcluding}
		for _, v := range vers {
			if v == "" {
				continue
			}
			v := strings.Replace(v, `\`, "", -1)
			if _, err := version.NewVersion(v); err != nil {
				return false
			}
		}
	}
	return true
}

func parseNvdJSONTime(strtime string) (t time.Time, err error) {
	layout := "2006-01-02T15:04Z"
	t, err = time.Parse(layout, strtime)
	if err != nil {
		return t, fmt.Errorf("Failed to parse time, time: %s, err: %s",
			strtime, err)
	}
	return
}
