package commands

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

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

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

	dbpath   string
	dbtype   string
	dumpPath string

	latest bool
	last2Y bool
	years  bool

	httpProxy string
}

// Name return subcommand name
func (*FetchJvnCmd) Name() string { return "fetchjvn" }

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

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

`
}

// SetFlags set flag
func (p *FetchJvnCmd) 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.latest, "latest", false,
		"Refresh JVN data for latest.")

	f.BoolVar(&p.last2Y, "last2y", false,
		"Refresh JVN data in the last two years.")

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

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

// Execute execute
func (p *FetchJvnCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
	c.Conf.Debug = p.debug
	c.Conf.Quiet = p.quiet
	c.Conf.DebugSQL = p.debugSQL
	c.Conf.DBPath = p.dbpath
	c.Conf.DBType = p.dbtype
	//  c.Conf.DumpPath = p.dumpPath
	c.Conf.HTTPProxy = p.httpProxy

	log.SetLogger(p.logDir, c.Conf.Quiet, c.Conf.Debug, p.logJSON)
	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 1998 to %d)", thisYear)
			return subcommands.ExitUsageError
		}
		for _, arg := range f.Args() {
			year, err := strconv.Atoi(arg)
			if err != nil || year < 1998 || time.Now().Year() < year {
				log.Errorf("Specify years to fetch (from 1998 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 -latest, -last2y 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 := jvn.FetchLatestFeedMeta(driver, years)
	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
	}

	log.Infof("Fetcling CVE information from JVN.")
	cves, err := jvn.FetchConvert(needUpdates)
	if err != nil {
		log.Errorf("Failed to fetch JVN: %s", err)
		return subcommands.ExitFailure
	}
	log.Infof("Fetched %d CVEs", len(cves))

	log.Infof("Inserting JVN into DB (%s).", driver.Name())
	if err := driver.InsertJvn(cves); err != nil {
		log.Fatalf("Failed to insert. dbpath: %s, err: %s", c.Conf.DBPath, err)
		return subcommands.ExitFailure
	}

	if err := jvn.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
}
