package wordpress

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	c "github.com/future-architect/vuls/config"
	"github.com/future-architect/vuls/models"
	"github.com/future-architect/vuls/util"
	version "github.com/hashicorp/go-version"
	"golang.org/x/xerrors"
)

//WpCveInfos is for wpvulndb's json
type WpCveInfos struct {
	ReleaseDate  string `json:"release_date"`
	ChangelogURL string `json:"changelog_url"`
	// Status        string `json:"status"`
	LatestVersion string `json:"latest_version"`
	LastUpdated   string `json:"last_updated"`
	// Popular         bool        `json:"popular"`
	Vulnerabilities []WpCveInfo `json:"vulnerabilities"`
	Error           string      `json:"error"`
}

//WpCveInfo is for wpvulndb's json
type WpCveInfo struct {
	ID        int    `json:"id"`
	Title     string `json:"title"`
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
	// PublishedDate string     `json:"published_date"`
	VulnType   string     `json:"vuln_type"`
	References References `json:"references"`
	FixedIn    string     `json:"fixed_in"`
}

//References is for wpvulndb's json
type References struct {
	URL     []string `json:"url"`
	Cve     []string `json:"cve"`
	Secunia []string `json:"secunia"`
}

// FillWordPress access to wpvulndb and fetch scurity alerts and then set to the given ScanResult.
// https://wpvulndb.com/
func FillWordPress(r *models.ScanResult, token string, wpVulnCaches *map[string]string) (int, error) {
	// Core
	ver := strings.Replace(r.WordPressPackages.CoreVersion(), ".", "", -1)
	if ver == "" {
		return 0, xerrors.New("Failed to get WordPress core version")
	}

	body, ok := searchCache(ver, wpVulnCaches)
	if !ok {
		url := fmt.Sprintf("https://wpvulndb.com/api/v3/wordpresses/%s", ver)
		var err error
		body, err = httpRequest(url, token)
		if err != nil {
			return 0, err
		}
		if body == "" {
			util.Log.Warnf("A result of REST access is empty: %s", url)
		}

		(*wpVulnCaches)[ver] = body
	}

	wpVinfos, err := convertToVinfos(models.WPCore, body)
	if err != nil {
		return 0, err
	}

	themes := r.WordPressPackages.Themes()
	plugins := r.WordPressPackages.Plugins()

	if c.Conf.WpIgnoreInactive {
		themes = removeInactives(themes)
		plugins = removeInactives(plugins)
	}

	// Themes
	for _, p := range themes {
		body, ok := searchCache(p.Name, wpVulnCaches)
		if !ok {
			url := fmt.Sprintf("https://wpvulndb.com/api/v3/themes/%s", p.Name)
			var err error
			body, err = httpRequest(url, token)
			if err != nil {
				return 0, err
			}
			(*wpVulnCaches)[p.Name] = body
		}

		if body == "" {
			continue
		}

		templateVinfos, err := convertToVinfos(p.Name, body)
		if err != nil {
			return 0, err
		}

		for _, v := range templateVinfos {
			for _, fixstat := range v.WpPackageFixStats {
				pkg, ok := r.WordPressPackages.Find(fixstat.Name)
				if !ok {
					continue
				}
				ok, err := match(pkg.Version, fixstat.FixedIn)
				if err != nil {
					return 0, xerrors.Errorf("Not a semantic versioning: %w", err)
				}
				if ok {
					wpVinfos = append(wpVinfos, v)
					util.Log.Infof("[match] %s installed: %s, fixedIn: %s", pkg.Name, pkg.Version, fixstat.FixedIn)
				} else {
					util.Log.Debugf("[miss] %s installed: %s, fixedIn: %s", pkg.Name, pkg.Version, fixstat.FixedIn)
				}
			}
		}
	}

	// Plugins
	for _, p := range plugins {
		body, ok := searchCache(p.Name, wpVulnCaches)
		if !ok {
			url := fmt.Sprintf("https://wpvulndb.com/api/v3/plugins/%s", p.Name)
			var err error
			body, err = httpRequest(url, token)
			if err != nil {
				return 0, err
			}
			(*wpVulnCaches)[p.Name] = body
		}

		if body == "" {
			continue
		}

		pluginVinfos, err := convertToVinfos(p.Name, body)
		if err != nil {
			return 0, err
		}

		for _, v := range pluginVinfos {
			for _, fixstat := range v.WpPackageFixStats {
				pkg, ok := r.WordPressPackages.Find(fixstat.Name)
				if !ok {
					continue
				}
				ok, err := match(pkg.Version, fixstat.FixedIn)
				if err != nil {
					return 0, xerrors.Errorf("Not a semantic versioning: %w", err)
				}
				if ok {
					wpVinfos = append(wpVinfos, v)
					//TODO Debugf
					util.Log.Infof("[match] %s installed: %s, fixedIn: %s", pkg.Name, pkg.Version, fixstat.FixedIn)
				} else {
					//TODO Debugf
					util.Log.Infof("[miss] %s installed: %s, fixedIn: %s", pkg.Name, pkg.Version, fixstat.FixedIn)
				}
			}
		}
	}

	for _, wpVinfo := range wpVinfos {
		if vinfo, ok := r.ScannedCves[wpVinfo.CveID]; ok {
			vinfo.CveContents[models.WPVulnDB] = wpVinfo.CveContents[models.WPVulnDB]
			vinfo.VulnType = wpVinfo.VulnType
			vinfo.Confidences = append(vinfo.Confidences, wpVinfo.Confidences...)
			vinfo.WpPackageFixStats = append(vinfo.WpPackageFixStats, wpVinfo.WpPackageFixStats...)
			r.ScannedCves[wpVinfo.CveID] = vinfo
		} else {
			r.ScannedCves[wpVinfo.CveID] = wpVinfo
		}
	}
	return len(wpVinfos), nil
}

func match(installedVer, fixedIn string) (bool, error) {
	v1, err := version.NewVersion(installedVer)
	if err != nil {
		return false, err
	}
	v2, err := version.NewVersion(fixedIn)
	if err != nil {
		return false, err
	}
	return v1.LessThan(v2), nil
}

func convertToVinfos(pkgName, body string) (vinfos []models.VulnInfo, err error) {
	if body == "" {
		return
	}
	// "pkgName" : CVE Detailed data
	pkgnameCves := map[string]WpCveInfos{}
	if err = json.Unmarshal([]byte(body), &pkgnameCves); err != nil {
		return nil, xerrors.Errorf("Failed to unmarshal %s. err: %w", body, err)
	}

	for _, v := range pkgnameCves {
		vs := extractToVulnInfos(pkgName, v.Vulnerabilities)
		vinfos = append(vinfos, vs...)
	}
	return vinfos, nil
}

func extractToVulnInfos(pkgName string, cves []WpCveInfo) (vinfos []models.VulnInfo) {
	for _, vulnerability := range cves {
		var cveIDs []string

		if len(vulnerability.References.Cve) == 0 {
			cveIDs = append(cveIDs, fmt.Sprintf("WPVDBID-%d", vulnerability.ID))
		}
		for _, cveNumber := range vulnerability.References.Cve {
			cveIDs = append(cveIDs, "CVE-"+cveNumber)
		}

		var refs []models.Reference
		for _, url := range vulnerability.References.URL {
			refs = append(refs, models.Reference{
				Link: url,
			})
		}

		for _, cveID := range cveIDs {
			vinfos = append(vinfos, models.VulnInfo{
				CveID: cveID,
				CveContents: models.NewCveContents(
					models.CveContent{
						Type:       models.WPVulnDB,
						CveID:      cveID,
						Title:      vulnerability.Title,
						References: refs,
					},
				),
				VulnType: vulnerability.VulnType,
				Confidences: []models.Confidence{
					models.WPVulnDBMatch,
				},
				WpPackageFixStats: []models.WpPackageFixStatus{{
					Name:    pkgName,
					FixedIn: vulnerability.FixedIn,
				}},
			})
		}
	}
	return
}

func httpRequest(url, token string) (string, error) {
	retry := 1
	util.Log.Debugf("%s", url)
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", fmt.Sprintf("Token token=%s", token))
loop:
	resp, err := new(http.Client).Do(req)
	if err != nil {
		return "", err
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode == 200 {
		return string(body), nil
	} else if resp.StatusCode == 404 {
		// This package is not in WPVulnDB
		return "", nil
	} else if resp.StatusCode == 429 && retry <= 3 {
		// 429 Too Many Requests
		util.Log.Debugf("sleep %d min(s): %s", retry, resp.Status)
		time.Sleep(time.Duration(retry) * time.Minute)
		retry++
		goto loop
	}
	return "", err
}

func removeInactives(pkgs models.WordPressPackages) (removed models.WordPressPackages) {
	for _, p := range pkgs {
		if p.Status == "inactive" {
			continue
		}
		removed = append(removed, p)
	}
	return removed
}

func searchCache(name string, wpVulnCaches *map[string]string) (string, bool) {
	value, ok := (*wpVulnCaches)[name]
	if ok {
		return value, true
	}
	return "", false
}
