package commands

import (
	"context"
	"flag"
	"os"
	"strconv"
	"time"

	"github.com/google/subcommands"
	c "github.com/kotakanbe/go-cve-dictionary/config"
	"github.com/kotakanbe/go-cve-dictionary/db"
	"github.com/kotakanbe/go-cve-dictionary/fetcher/nvd"
	nvdjson "github.com/kotakanbe/go-cve-dictionary/fetcher/nvd/json"
	nvdxml "github.com/kotakanbe/go-cve-dictionary/fetcher/nvd/xml"
	log "github.com/kotakanbe/go-cve-dictionary/log"
	"github.com/kotakanbe/go-cve-dictionary/models"
	"github.com/kotakanbe/go-cve-dictionary/util"
)

// FetchNvdCmd is Subcommand for fetch Nvd information.
type FetchNvdCmd struct {
	debug    bool
	debugSQL bool
	quiet    bool
	logDir   string
	logJSON  bool

	dbpath string
	dbtype string

	Latest bool
	last2Y bool
	years  bool

	httpProxy string

	nvdXML bool
}

// Name return subcommand name
func (*FetchNvdCmd) Name() string { return "fetchnvd" }

// Synopsis return synopsis
func (*FetchNvdCmd) Synopsis() string { return "Fetch Vulnerability dictionary from NVD" }

// Usage return usage
func (*FetchNvdCmd) Usage() string {
	return `fetchnvd:
	fetchnvd
		[-latest]
		[-last2y]
		[-years] 2015 2016 ...
		[-dbtype=mysql|postgres|sqlite3|redis]
		[-dbpath=$PWD/cve.sqlite3 or connection string]
		[-http-proxy=http://192.168.0.1:8080]
		[-debug]
		[-debug-sql]
		[-quiet]
		[-xml]
		[-log-dir=/path/to/log]
		[-log-json]

For the first time, run the blow command to fetch data for entire period. (It takes about 10 minutes)
   $ for i in ` + "`seq 2002 $(date +\"%Y\")`;" + ` do go-cve-dictionary fetchnvd -years $i; done

`
}

// SetFlags set flag
func (p *FetchNvdCmd) SetFlags(f *flag.FlagSet) {
	f.BoolVar(&p.debug, "debug", false, "debug mode")
	f.BoolVar(&p.debugSQL, "debug-sql", false, "SQL debug mode")
	f.BoolVar(&p.quiet, "quiet", false, "quiet mode (no output)")

	defaultLogDir := util.GetDefaultLogDir()
	f.StringVar(&p.logDir, "log-dir", defaultLogDir, "/path/to/log")
	f.BoolVar(&p.logJSON, "log-json", false, "output log as JSON")

	pwd := os.Getenv("PWD")
	f.StringVar(&p.dbpath, "dbpath", pwd+"/cve.sqlite3",
		"/path/to/sqlite3 or SQL connection string")

	f.StringVar(&p.dbtype, "dbtype", "sqlite3",
		"Database type to store data in (sqlite3, mysql, postgres or redis supported)")

	f.BoolVar(&p.last2Y, "last2y", false,
		"Refresh NVD data in the last two years recent and modified feeds")

	f.BoolVar(&p.Latest, "latest", false,
		"Refresh recent and modified feeds")

	f.BoolVar(&p.years, "years", false,
		"Refresh NVD data of specific years")

	f.BoolVar(&p.nvdXML, "xml", false,
		"Download [XML](https://nvd.nist.gov/vuln/data-feeds#XML_FEED) Vulnerability Feeds. (default [JSON](https://nvd.nist.gov/vuln/data-feeds#JSON_FEED))")

	f.StringVar(
		&p.httpProxy,
		"http-proxy",
		"",
		"http://proxy-url:port (default: empty)",
	)
}

// Execute execute
func (p *FetchNvdCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
	c.Conf.Debug = p.debug
	c.Conf.Quiet = p.quiet
	log.SetLogger(p.logDir, c.Conf.Quiet, c.Conf.Debug, p.logJSON)

	c.Conf.DebugSQL = p.debugSQL
	c.Conf.DBPath = p.dbpath
	c.Conf.DBType = p.dbtype
	c.Conf.HTTPProxy = p.httpProxy
	c.Conf.NVDXML = p.nvdXML

	if !c.Conf.Validate() {
		return subcommands.ExitUsageError
	}

	years := []int{}
	thisYear := time.Now().Year()

	switch {
	case p.Latest:
		years = append(years, c.Latest)
	case p.last2Y:
		for i := 0; i < 2; i++ {
			years = append(years, thisYear-i)
		}
		years = append(years, c.Latest)
	case p.years:
		if len(f.Args()) == 0 {
			log.Errorf("Specify years to fetch (from 2002 to %d)", thisYear)
			return subcommands.ExitUsageError
		}
		for _, arg := range f.Args() {
			year, err := strconv.Atoi(arg)
			if err != nil || year < 2002 || time.Now().Year() < year {
				log.Errorf("Specify years to fetch (from 2002 to %d), arg: %s", thisYear, arg)
				return subcommands.ExitUsageError
			}
			found := false
			for _, y := range years {
				if y == year {
					found = true
					break
				}
			}
			if !found {
				years = append(years, year)
			}
		}
		years = append(years, c.Latest)
	default:
		log.Errorf("specify -last2y, -latest or -years")
		return subcommands.ExitUsageError
	}

	driver, locked, err := db.NewDB(c.Conf.DBType, c.Conf.DBPath, c.Conf.DebugSQL)
	if err != nil {
		if locked {
			log.Errorf("Failed to Open DB. Close DB connection before fetching: %s", err)
			return subcommands.ExitFailure
		}
		log.Errorf("%s", err)
		return subcommands.ExitFailure
	}
	defer driver.CloseDB()

	metas, err := nvd.FetchLatestFeedMeta(driver, years, c.Conf.NVDXML)
	if err != nil {
		log.Errorf("%s", err)
		return subcommands.ExitFailure
	}

	if len(metas) == 0 {
		log.Errorf("No meta files fetched")
		return subcommands.ExitFailure
	}

	//TODO use meta.Status()
	needUpdates := []models.FeedMeta{}
	for _, m := range metas {
		if m.Newly() {
			needUpdates = append(needUpdates, m)
			log.Infof("     Newly: %s", m.URL)
		} else if m.OutDated() {
			needUpdates = append(needUpdates, m)
			log.Infof("  Outdated: %s", m.URL)
		} else {
			log.Infof("Up to date: %s", m.URL)
		}
	}

	if len(needUpdates) == 0 {
		log.Infof("Already up to date")
		return subcommands.ExitSuccess
	}

	cves := []models.CveDetail{}
	if c.Conf.NVDXML {
		cves, err = nvdxml.FetchConvert(needUpdates)
	} else {
		cves, err = nvdjson.FetchConvert(needUpdates)
	}
	if err != nil {
		log.Errorf("%s", err)
		return subcommands.ExitFailure
	}

	log.Infof("Fetched %d CVEs", len(cves))
	log.Infof("Inserting NVD into DB (%s).", driver.Name())

	if c.Conf.NVDXML {
		if err := driver.InsertNvdXML(cves); err != nil {
			log.Errorf("Failed to insert. dbpath: %s, err: %s",
				c.Conf.DBPath, err)
			return subcommands.ExitFailure
		}
	} else {
		if err := driver.InsertNvdJSON(cves); err != nil {
			log.Errorf("Failed to insert. dbpath: %s, err: %s",
				c.Conf.DBPath, err)
			return subcommands.ExitFailure
		}
	}

	if err := nvd.UpdateMeta(driver, needUpdates); err != nil {
		log.Fatalf("Failed to Update meta. dbpath: %s, err: %s", c.Conf.DBPath, err)
		return subcommands.ExitFailure
	}
	return subcommands.ExitSuccess
}
