package main

import (
	"encoding/json"
	"sort"
	"time"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"

	"io/ioutil"
	"net/http"

	"errors"
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/BurntSushi/toml"
	"github.com/go-openapi/strfmt"
	"github.com/leekchan/accounting"
	"github.com/logrusorgru/aurora"
	"github.com/syndtr/goleveldb/leveldb"

	terminal "github.com/wayneashleyberry/terminal-dimensions"

	InternalUtils "./internals"

	ESI "git.inf3.xyz/tschwery/eve-goclient.git/client"
	ESIModels "git.inf3.xyz/tschwery/eve-goclient.git/models"

	ESIClones "git.inf3.xyz/tschwery/eve-goclient.git/client/clones"
	ESIIndustry "git.inf3.xyz/tschwery/eve-goclient.git/client/industry"
	ESILocation "git.inf3.xyz/tschwery/eve-goclient.git/client/location"
	ESIMarket "git.inf3.xyz/tschwery/eve-goclient.git/client/market"
	ESIPlanetaryInteraction "git.inf3.xyz/tschwery/eve-goclient.git/client/planetary_interaction"
	ESISkills "git.inf3.xyz/tschwery/eve-goclient.git/client/skills"
	ESIUniverse "git.inf3.xyz/tschwery/eve-goclient.git/client/universe"
	ESIWallet "git.inf3.xyz/tschwery/eve-goclient.git/client/wallet"

	httptransport "github.com/go-openapi/runtime/client"
)

// Character - Structure to save the verification data.
type Character struct {
	CharacterID   int32
	CharacterName string
	ExpiresOn     string
}

type configurationFile struct {
	ClientID     string
	ClientSecret string
}

var (
	defaultDateFormat = "_2 Jan 2006, 15:04"

	cfgFilePath  = flag.String("config", "configuration.toml", "Path to the configuration file.")
	cacheDBPath  = flag.String("cache", "cache.ldb", "Path to the cache leveldb database.")
	insecureFlag = flag.Bool("insecure", false, "Do not check the HTTPS certificate")
	logCalls     = flag.Bool("httplog", false, "Log HTTP calls")
	renewToken   = flag.Bool("renewToken", false, "Renew the token even if it is still valid")
)

var ctx = context.Background()
var messages = make(chan *oauth2.Token)
var cache InternalUtils.CacheConn

func main() {
	flag.Parse()

	logfilename := fmt.Sprintf("log-%s.log", time.Now().Format("2006-01-02"))
	f, err := os.OpenFile(logfilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalf("error opening file: %v", err)
	}
	defer f.Close()

	log.SetOutput(f)

	var cacheDBerr error
	cacheDB, cacheDBerr := leveldb.OpenFile(*cacheDBPath, nil)
	if cacheDBerr != nil {
		fmt.Println("Unable to initialize the LevelDB cache.")
		log.Fatal(cacheDBerr)
	}

	cache.SetCache(cacheDB)

	httpConfiguration, cErr := readConfigurationFile()
	if cErr != nil {
		fmt.Println("Missing configuration file.")
		log.Fatal(cErr)
	}

	cToken := getDatabaseToken()

	if cToken == nil || *renewToken {
		cToken = getNewAuthorizationToken(httpConfiguration)
	}

	httpConfiguration.ConnectionToken = cToken

	client := InternalUtils.GetDefaultClient(cacheDB, httpConfiguration)

	m, err := getCharacterInfo(client)
	if err != nil {
		log.Println(err)
		cToken = getNewAuthorizationToken(httpConfiguration)
		httpConfiguration.ConnectionToken = cToken

		client = InternalUtils.GetDefaultClient(cacheDB, httpConfiguration)
		m, err = getCharacterInfo(client)
		if err != nil {
			log.Fatal(err)
		}
	}

	transport := httptransport.NewWithClient("esi.tech.ccp.is", "/latest", []string{"https"}, client)
	//transport.Debug = true

	swaggerclient := ESI.New(transport, strfmt.Default)

	charInfoChan := make(chan string)
	cloneInfoChan := make(chan string)
	planetInfoChan := make(chan string)
	skillsInfoChan := make(chan string)
	marketInfoChan := make(chan string)
	industryInfoChan := make(chan string)
	transactionsInfoChan := make(chan string)

	go func() {
		charInfoChan <- printCharacterInformation(swaggerclient, m)
		close(charInfoChan)
	}()

	go func() {
		cloneInfoChan <- printClonesInformation(swaggerclient, m)
		close(cloneInfoChan)
	}()

	go func() {
		planetInfoChan <- printCharacterPlanets(swaggerclient, m)
		close(planetInfoChan)
	}()

	go func() {
		marketInfoChan <- printCharacterMarketOrders(swaggerclient, m)
		close(marketInfoChan)
	}()

	go func() {
		industryInfoChan <- printCharacterIndustryJobs(swaggerclient, m)
		close(industryInfoChan)
	}()

	go func() {
		skillsInfoChan <- printCharacterSkillQueue(swaggerclient, m)
		close(skillsInfoChan)
	}()

	go func() {
		transactionsInfoChan <- printCharacterTransactions(swaggerclient, m)
		close(transactionsInfoChan)
	}()

	for msg := range charInfoChan {
		fmt.Print(msg)
	}

	fmt.Printf("\n\nClones\n")
	for msg := range cloneInfoChan {
		fmt.Print(msg)
	}

	fmt.Printf("\n\nPlanetary interaction\n")
	for msg := range planetInfoChan {
		fmt.Print(msg)
	}

	fmt.Printf("\n\nSkill queue\n")
	for msg := range skillsInfoChan {
		fmt.Print(msg)
	}

	fmt.Printf("\n\nMarket orders\n")
	for msg := range marketInfoChan {
		fmt.Print(msg)
	}

	fmt.Printf("\n\nIndustry Jobs\n")
	for msg := range industryInfoChan {
		fmt.Print(msg)
	}

	fmt.Printf("\n\nTransactions\n")
	for msg := range transactionsInfoChan {
		fmt.Print(msg)
	}
}

func readConfigurationFile() (*InternalUtils.HTTPConfiguration, error) {
	var config configurationFile
	if _, err := toml.DecodeFile(*cfgFilePath, &config); err != nil {
		log.Println(err)

		return nil, err
	}

	if config.ClientID == "" || config.ClientSecret == "" {
		log.Println("Missing ClientID or ClientSecret configuration option in configuration file" + *cfgFilePath + ".")
		err := errors.New("Missing ClientID or ClientSecret in configuration file " + *cfgFilePath + ".")

		return nil, err
	}

	var httpConfig InternalUtils.HTTPConfiguration
	httpConfig.ClientID = config.ClientID
	httpConfig.ClientSecret = config.ClientSecret

	return &httpConfig, nil
}

func getCharacterInfo(client *http.Client) (*Character, error) {

	req, _ := http.NewRequest("GET", "https://login.eveonline.com/oauth/verify", nil)
	response, errDo := client.Do(req)
	if errDo != nil {
		return nil, errDo
	}

	defer response.Body.Close()
	contents, errRead := ioutil.ReadAll(response.Body)
	if errRead != nil {
		log.Printf("Read error: %s\n", errRead)
		return nil, errRead
	}

	var m Character
	errJSON := json.Unmarshal(contents, &m)
	if errJSON != nil {
		log.Printf("Unmarshalling error: %s\n", errJSON)
		return nil, errJSON
	}

	if m.CharacterID == 0 || m.CharacterName == "" {
		log.Printf("Invalid return value from verify call: %s", contents)
		return nil, errors.New("Invalid token")
	}

	return &m, nil
}

func printCharacterSkillQueue(swaggerclient *ESI.App, m *Character) string {
	callParam := ESISkills.NewGetCharactersCharacterIDSkillqueueParams()
	callParam.WithCharacterID(m.CharacterID)

	skillqueueresp, skillerr := swaggerclient.Skills.GetCharactersCharacterIDSkillqueue(callParam, nil)
	if skillerr != nil {
		log.Fatalf("Got error on GetCharactersCharacterIDSkillqueue: %T %s", skillerr, skillerr)
	}

	skillqueue := skillqueueresp.Payload

	now := time.Now()

	snIds := make([]int32, 0, len(skillqueue))
	for _, skill := range skillqueue {
		snIds = append(snIds, *skill.SkillID)
	}
	skillNames := getUniverseNames(swaggerclient, &snIds)

	var content string

	maxSkillLength := 0
	for _, skill := range skillqueue {
		name := skillNames[*skill.SkillID]
		if maxSkillLength < len(name) {
			maxSkillLength = len(name)
		}
	}

	for _, skill := range skillqueue {
		// element is the element from someSlice for where we are
		name := skillNames[*skill.SkillID]

		finishDate := time.Time(skill.FinishDate)
		startDate := time.Time(skill.StartDate)

		skillDuration := finishDate.Sub(startDate)
		finishDuration := finishDate.Sub(now)

		maxSkillFormat := fmt.Sprintf("%d", maxSkillLength)

		// see https://github.com/ccpgames/esi-issues/issues/113
		// The queue is only updated when the user logs in with the client
		// we thus need to do the computations and filtering ourselves
		if finishDate.Before(time.Now()) {
			content = content + fmt.Sprintf("✔ %"+maxSkillFormat+"s %d\n",
				name,
				*skill.FinishedLevel,
			)
			continue
		}

		if startDate.Before(time.Now()) {
			content = content + fmt.Sprintf("%s %"+maxSkillFormat+"s %d, %s - ends on %s (%s)\n",
				aurora.Green("➠").Bold(),
				name,
				*skill.FinishedLevel,
				formatDuration(skillDuration, 2),
				time.Time(skill.FinishDate).Format(defaultDateFormat),
				formatDuration(finishDuration, 3),
			)
			continue
		}

		content = content + fmt.Sprintf("  %"+maxSkillFormat+"s %d, %s - ends on %s (%s)\n",
			name,
			*skill.FinishedLevel,
			formatDuration(skillDuration, 2),
			time.Time(skill.FinishDate).Format(defaultDateFormat),
			formatDuration(finishDuration, 3),
		)
	}

	return content
}

func printCharacterTransactions(swaggerclient *ESI.App, m *Character) string {
	callParam := ESIWallet.NewGetCharactersCharacterIDWalletTransactionsParams()
	callParam.WithCharacterID(m.CharacterID)

	esiresponse, esierr := swaggerclient.Wallet.GetCharactersCharacterIDWalletTransactions(callParam, nil)
	if esierr != nil {
		fmt.Println("Error while getting the wallet information")
		log.Fatalf("Got error on GetCharactersCharacterIDWalletTransactions: %T %s", esierr, esierr)
	}

	transactions := esiresponse.Payload

	ac := accounting.Accounting{Symbol: "", Precision: 2, Thousand: "'"}

	var content string

	itemIds := make([]int32, 0)

	for _, transaction := range transactions {
		itemIds = append(itemIds, *transaction.TypeID)
		itemIds = append(itemIds, *transaction.ClientID)
	}

	universeNames := getUniverseNames(swaggerclient, &itemIds)

	for _, transaction := range transactions[:20] {

		locationName, locationErr := getStructureStationInfo(swaggerclient, *transaction.LocationID)
		if locationErr != nil {
			fmt.Println("Error while getting the location information")
			log.Fatalf("Got error on getStructureStationInfo: %T %s", locationErr, locationErr)
		}

		status := fmt.Sprint(aurora.Red("✘").Bold())
		if *transaction.IsBuy {
			status = fmt.Sprint(aurora.Green("✔").Bold())
		}

		content = content + fmt.Sprintf(" %s %s ISK% 15s,% 7d %s, %s, %s",
			status,
			time.Time(*transaction.Date).Format(defaultDateFormat),
			ac.FormatMoney(*transaction.UnitPrice*float32(*transaction.Quantity)),
			*transaction.Quantity,
			universeNames[*transaction.TypeID],
			locationName.Name,
			universeNames[*transaction.ClientID],
		)

		content = content + "\n"
	}

	return content
}

func formatDuration(duration time.Duration, daysLength int32) string {
	days := int32(duration.Hours()) / 24
	hours := int32(duration.Minutes()) / 60 % 24
	minutes := int32(duration.Minutes()) % 60

	return fmt.Sprintf("%"+fmt.Sprintf("%d", daysLength)+"dd %02d:%02d", days, hours, minutes)
}

func printCharacterInformation(swaggerclient *ESI.App, m *Character) string {
	callParam := ESIWallet.NewGetCharactersCharacterIDWalletParams()
	callParam.WithCharacterID(m.CharacterID)

	esiresponse, esierr := swaggerclient.Wallet.GetCharactersCharacterIDWallet(callParam, nil)
	if esierr != nil {
		fmt.Println("Error while getting the wallet information")
		log.Fatalf("Got error on GetCharactersCharacterIDWallet: %T %s", esierr, esierr)
	}

	wallet := esiresponse.Payload

	ac := accounting.Accounting{Symbol: "ISK ", Precision: 0, Thousand: "'"}

	var content string
	content = content + fmt.Sprintf("Name: %s (%d)\n", m.CharacterName, m.CharacterID)

	content = content + fmt.Sprintf("Wallet: %s\n", ac.FormatMoney(wallet))

	posCallParam := ESILocation.NewGetCharactersCharacterIDLocationParams()
	posCallParam.WithCharacterID(m.CharacterID)

	posresponse, poserr := swaggerclient.Location.GetCharactersCharacterIDLocation(posCallParam, nil)
	if poserr != nil {
		fmt.Println("Error while getting the current character location.")
		log.Fatalf("Got error on GetCharactersCharacterIDLocation: %s", poserr)
	}

	position := posresponse.Payload

	itemIds := make([]int32, 0)
	itemIds = append(itemIds, *position.SolarSystemID)
	if position.StationID != 0 {
		itemIds = append(itemIds, position.StationID)
	}

	universeNames := getUniverseNames(swaggerclient, &itemIds)

	stationName := ""
	if position.StationID != 0 {
		stationName = universeNames[position.StationID]
	}

	if position.StructureID != 0 {
		retName, retErr := getStructureStationInfo(swaggerclient, position.StructureID)
		if retErr == nil {
			stationName = retName.Name
		}
	}

	content = content + fmt.Sprintf("Currently in %s - %s\n",
		universeNames[*position.SolarSystemID],
		stationName,
	)

	return content
}

func printClonesInformation(swaggerclient *ESI.App, m *Character) string {
	cloCallParams := ESIClones.NewGetCharactersCharacterIDClonesParams()
	cloCallParams.WithCharacterID(m.CharacterID)

	cloresponse, cloerr := swaggerclient.Clones.GetCharactersCharacterIDClones(cloCallParams, nil)
	if cloerr != nil {
		fmt.Println("Error while getting the current character clones.")
		log.Fatalf("Got error on GetCharactersCharacterIDClones: %s", cloerr)
	}

	var content string

	clones := cloresponse.Payload.JumpClones

	for _, clone := range clones {
		sInfo, sErr := getStructureStationInfo(swaggerclient, clone.LocationID)
		if sErr != nil {
			fmt.Printf("Error on structure information read on structure %s\n", clone.LocationID)
			log.Fatalf("Got error on getStructureStationInfo: %T %s", sErr, sErr)
		}

		content = content + fmt.Sprintf(" %s, %s, %s\n    ",
			sInfo.Name,
			sInfo.System.ConstellationName,
			sInfo.System.RegionName,
		)

		implantNames := getUniverseNames(swaggerclient, &clone.Implants)
		for _, implant := range clone.Implants {
			implantName := implantNames[implant]
			content = content + fmt.Sprintf(" %s, ", implantName)
		}

		content = content + "\n"
	}

	return content
}

type structureInfo struct {
	ID       int64
	Name     string
	SystemID int32
	System   solarSystemInfo
}

func getStructureStationInfo(swaggerclient *ESI.App, ID int64) (*structureInfo, error) {
	locationName := cache.GetCachedDataSub64(ID, "name")
	locationSystem := cache.GetCachedDataIntSub64(ID, "system")

	if locationName != "" && locationSystem > 0 {
		var sInfo structureInfo

		sInfo.ID = ID
		sInfo.Name = locationName
		sInfo.System = getSolarSystemInformation(swaggerclient, locationSystem)

		return &sInfo, nil
	}

	if ID <= 1000000000000 {
		stacallParams := ESIUniverse.NewGetUniverseStationsStationIDParams()
		stacallParams.WithStationID(int32(ID))

		staresponse, staerr := swaggerclient.Universe.GetUniverseStationsStationID(stacallParams)
		if staerr != nil {
			log.Printf("Got error on GetUniverseStationsStationID: %s", staerr)
			return nil, staerr
		}

		staInfo := staresponse.Payload

		var sInfo structureInfo

		sInfo.ID = int64(*staInfo.StationID)
		sInfo.Name = *staInfo.Name
		sInfo.SystemID = *staInfo.SystemID
		sInfo.System = getSolarSystemInformation(swaggerclient, sInfo.SystemID)

		cache.PutCacheDataSub64(sInfo.ID, "name", sInfo.Name)
		cache.PutCacheDataIntSub64(sInfo.ID, "system", sInfo.SystemID)

		return &sInfo, nil
	}

	strcallParams := ESIUniverse.NewGetUniverseStructuresStructureIDParams()
	strcallParams.WithStructureID(ID)

	strresponse, strerr := swaggerclient.Universe.GetUniverseStructuresStructureID(strcallParams, nil)
	if strerr != nil {
		log.Printf("Got error on GetUniverseStructuresStructureID: %s", strerr)
		return nil, strerr
	}

	strInfo := strresponse.Payload

	var sInfo structureInfo

	sInfo.ID = ID
	sInfo.Name = *strInfo.Name
	sInfo.SystemID = *strInfo.SolarSystemID
	sInfo.System = getSolarSystemInformation(swaggerclient, sInfo.SystemID)

	cache.PutCacheDataSub64(sInfo.ID, "name", sInfo.Name)
	cache.PutCacheDataIntSub64(sInfo.ID, "system", sInfo.SystemID)

	return &sInfo, nil
}

type byPIPinType ESIModels.GetCharactersCharacterIDPlanetsPlanetIDOKBodyPins

func (s byPIPinType) Len() int {
	return len(s)
}
func (s byPIPinType) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}
func (s byPIPinType) Less(i, j int) bool {
	a := s[i]
	b := s[j]

	if *a.TypeID != *b.TypeID {
		return *a.TypeID < *b.TypeID
	}

	if a.ExtractorDetails != nil {
		return a.ExtractorDetails.ProductTypeID < a.ExtractorDetails.ProductTypeID
	}

	if a.SchematicID > 0 {
		return a.SchematicID < b.SchematicID
	}

	return *a.PinID < *b.PinID
}

func printCharacterPlanets(swaggerclient *ESI.App, m *Character) string {

	callParam := ESIPlanetaryInteraction.NewGetCharactersCharacterIDPlanetsParams()
	callParam.WithCharacterID(m.CharacterID)

	esiresponse, esierr := swaggerclient.PlanetaryInteraction.GetCharactersCharacterIDPlanets(callParam, nil)
	if esierr != nil {
		fmt.Println("Error while getting the planetary interaction information.")
		log.Fatalf("Got error on GetCharactersCharacterIDPlanets: %T %s", esierr, esierr)
	}

	planets := esiresponse.Payload

	var content string

	now := time.Now()

	for _, planet := range planets {
		pcallParam := ESIPlanetaryInteraction.NewGetCharactersCharacterIDPlanetsPlanetIDParams()
		pcallParam.WithCharacterID(m.CharacterID).WithPlanetID(*planet.PlanetID)

		planetSystemInfo := getSolarSystemInformation(swaggerclient, *planet.SolarSystemID)

		planetName := getPlanetInformation(swaggerclient, *planet.PlanetID)

		content = content + fmt.Sprintf("Planet %s, %s, %s - Type %s, Lvl %d - Updated %s\n",
			planetName.PlanetName,
			planetSystemInfo.ConstellationName,
			planetSystemInfo.RegionName,
			*planet.PlanetType,
			*planet.UpgradeLevel,
			time.Time(*planet.LastUpdate).Format(defaultDateFormat),
		)

		pesiresponse, pesierr := swaggerclient.PlanetaryInteraction.GetCharactersCharacterIDPlanetsPlanetID(pcallParam, nil)
		if pesierr != nil {
			fmt.Println("    Error while getting the planetary interaction information")
			log.Printf("Error on CharactersCharacterIDPlanetsPlanetID: %s\n", pesierr)
			continue
		}

		pins := pesiresponse.Payload.Pins
		sort.Sort(byPIPinType(pins))

		ptIds := make([]int32, 0, len(pins))
		for _, pin := range pins {
			if pin.ExtractorDetails != nil {
				ptIds = append(ptIds, pin.ExtractorDetails.ProductTypeID)
			}
		}
		pinNames := getUniverseNames(swaggerclient, &ptIds)

		for _, pin := range pins {
			if pin.ExtractorDetails != nil {
				status := fmt.Sprint(aurora.Red("✘").Bold())
				statuscomment := fmt.Sprint(aurora.Red("expired").Bold())
				duration := now.Sub(time.Time(pin.ExpiryTime))
				if time.Time(pin.ExpiryTime).After(now) {
					status = fmt.Sprint(aurora.Green("✔").Bold())
					statuscomment = "expires"
					duration = time.Time(pin.ExpiryTime).Sub(now)
				}

				content = content + fmt.Sprintf(" %s Extractor %4ds cycle, %s, %d per cycle, %s %s (%s)\n",
					status,
					pin.ExtractorDetails.CycleTime,
					pinNames[pin.ExtractorDetails.ProductTypeID],
					pin.ExtractorDetails.QtyPerCycle,
					statuscomment,
					time.Time(pin.ExpiryTime).Format(defaultDateFormat),
					formatDuration(duration, 2),
				)
			} else if pin.SchematicID != 0 {

				// Get the schematic from ESI and cache it
				schematicInfo, serr := getSchematicsInformation(swaggerclient, pin.SchematicID)
				if serr != nil {
					log.Printf("Error on getSchematicsInformation: %T, %s\n", serr, serr)
					content = content + fmt.Sprintf(" ✔ Factory   ????? cycle, ?????\n")
				} else {
					content = content + fmt.Sprintf(" ✔ Factory   %4ds cycle, %s\n",
						schematicInfo.CycleTime,
						schematicInfo.SchematicName,
					)
				}
			}
		}
	}

	return content
}

func printCharacterMarketOrders(swaggerclient *ESI.App, m *Character) string {
	cloCallParams := ESIMarket.NewGetCharactersCharacterIDOrdersParams()
	cloCallParams.WithCharacterID(m.CharacterID)

	cloresponse, cloerr := swaggerclient.Market.GetCharactersCharacterIDOrders(cloCallParams, nil)
	if cloerr != nil {
		fmt.Println("Error while getting the current character market orders.")
		log.Fatalf("Got error on GetCharactersCharacterIDOrders: %s", cloerr)
	}

	ac := accounting.Accounting{Symbol: "", Precision: 2, Thousand: "'"}

	var content string

	orders := cloresponse.Payload

	ptIds := make([]int32, 0, len(orders))
	for _, order := range orders {
		ptIds = append(ptIds, *order.TypeID)
	}
	pinNames := getUniverseNames(swaggerclient, &ptIds)

	maxWidth, _ := terminal.Width()
	// We need to add the length of the colors
	maxWidth = maxWidth + uint(len(fmt.Sprint(aurora.Red("")))*3)

	lineFormat := fmt.Sprintf("%%.%ds\n", maxWidth)

	for _, order := range orders {
		expirationDate := time.Time(*order.Issued).Add(time.Duration(*order.Duration) * time.Hour * 24)
		remainingAmount := *order.Price * float32(*order.VolumeRemain)
		remainingDuration := expirationDate.Sub(time.Now())

		buySellStatus := "↟"
		if *order.IsBuyOrder {
			buySellStatus = "↡"
		}

		var status string

		if remainingDuration.Hours() <= 0 {
			status = fmt.Sprint(aurora.Red(buySellStatus).Bold())
		} else if remainingDuration.Hours() <= 72 {
			status = fmt.Sprint(aurora.Magenta(buySellStatus).Bold())
		} else {
			status = fmt.Sprint(aurora.Green(buySellStatus).Bold())
		}

		duration := fmt.Sprintf("%d/%d", remainingDuration/time.Hour/24, *order.Duration)
		pctDaysRemaining := float32(remainingDuration/time.Hour/24) / float32(*order.Duration)
		if pctDaysRemaining <= 0.25 {
			duration = fmt.Sprint(aurora.Red(duration))
		} else if pctDaysRemaining <= 0.5 {
			duration = fmt.Sprint(aurora.Brown(duration))
		} else {
			duration = fmt.Sprint(aurora.Green(duration))
		}

		quantity := fmt.Sprintf("%d/%d", *order.VolumeRemain, *order.VolumeTotal)
		pctRemaining := float32(*order.VolumeRemain) / float32(*order.VolumeTotal)
		if pctRemaining <= 0.25 {
			quantity = fmt.Sprint(aurora.Red(quantity))
		} else if pctRemaining <= 0.5 {
			quantity = fmt.Sprint(aurora.Brown(quantity))
		} else {
			quantity = fmt.Sprint(aurora.Green(quantity))
		}

		sInfo, sErr := getStructureStationInfo(swaggerclient, int64(*order.LocationID))
		if sErr != nil {
			fmt.Printf("Error on structure information read on structure %s\n", order.LocationID)
			log.Fatalf("Got error on getStructureStationInfo: %T %s", sErr, sErr)
		}

		line := fmt.Sprintf("  %s % 25.25s ISK % 12s ISK % 13s (%s) (%s) %s",
			status,
			pinNames[*order.TypeID],
			ac.FormatMoney(*order.Price),
			ac.FormatMoney(remainingAmount),
			quantity,
			duration,
			sInfo.Name,
		)

		content = content + fmt.Sprintf(lineFormat, line)
	}

	return content
}

func printCharacterIndustryJobs(swaggerclient *ESI.App, m *Character) string {
	cloCallParams := ESIIndustry.NewGetCharactersCharacterIDIndustryJobsParams()
	cloCallParams.WithCharacterID(m.CharacterID)

	cloresponse, cloerr := swaggerclient.Industry.GetCharactersCharacterIDIndustryJobs(cloCallParams, nil)
	if cloerr != nil {
		fmt.Println("Error while getting the current character industry jobs.")
		log.Fatalf("Got error on GetCharactersCharacterIDIndustryJobs: %s", cloerr)
	}

	ac := accounting.Accounting{Symbol: "", Precision: 0, Thousand: "'"}

	jobs := cloresponse.Payload

	ptIds := make([]int32, 0, len(jobs))
	for _, job := range jobs {
		if job.ProductTypeID > 0 {
			ptIds = append(ptIds, job.ProductTypeID)
		}
	}
	productNames := getUniverseNames(swaggerclient, &ptIds)

	maxWidth, _ := terminal.Width()
	// We need to add the length of the colors
	maxWidth = maxWidth + uint(len(fmt.Sprint(aurora.Red("")))*1)

	lineFormat := fmt.Sprintf("%%.%ds\n", maxWidth)

	priresponse, prierr := swaggerclient.Market.GetMarketsPrices(nil)
	if prierr != nil {
		fmt.Println("Error while getting the current market prices.")
		log.Fatalf("Got error on GetMarketsPrices: %s", cloerr)
	}

	prices := priresponse.Payload
	priIds := make(map[int32]float32)
	for _, price := range prices {
		priIds[*price.TypeID] = price.AdjustedPrice
	}

	var content string
	for _, job := range jobs {
		var status string

		if *job.Status == "delivered" || *job.Status == "cancelled" {
			continue
		} else if *job.Status == "active" {
			status = "➠"
		} else if *job.Status == "paused" {
			status = "↟"
		} else if *job.Status == "ready" {
			status = "✔"
		} else {
			status = "✘"
		}

		remainingDuration := time.Time(*job.EndDate).Sub(time.Now())

		if remainingDuration.Hours() <= 0 {
			status = fmt.Sprint(aurora.Red(status).Bold())
		} else if remainingDuration.Hours() <= 72 {
			status = fmt.Sprint(aurora.Magenta(status).Bold())
		} else {
			status = fmt.Sprint(aurora.Green(status).Bold())
		}

		var durationInfo string
		if remainingDuration < 0 {
			durationInfo = "         "
		} else {
			durationInfo = formatDuration(remainingDuration, 2)
		}

		sInfo, sErr := getStructureStationInfo(swaggerclient, int64(*job.FacilityID))
		if sErr != nil {
			fmt.Printf("Error on structure information read on structure %s\n", *job.FacilityID)
			log.Fatalf("Got error on getStructureStationInfo: %T %s", sErr, sErr)
		}

		totalEstimatedPrice := float32(*job.Runs) * priIds[job.ProductTypeID]

		line := fmt.Sprintf("  %s % 5d %- 20.20s ~ISK % 12s %s (%s) %s",
			status,
			*job.Runs,
			productNames[job.ProductTypeID],
			ac.FormatMoney(totalEstimatedPrice),
			time.Time(*job.EndDate).Format(defaultDateFormat),
			durationInfo,
			sInfo.Name,
		)

		content = content + fmt.Sprintf(lineFormat, line)
	}

	return content
}

func getUniverseNames(swaggerclient *ESI.App, itemIds *[]int32) map[int32]string {
	itemNames := make(map[int32]string)
	itemMissingNames := make(map[int32]string)

	for _, itemID := range *itemIds {
		itemName := cache.GetCachedDataSub(itemID, "name")
		if itemName != "" {
			itemNames[itemID] = itemName
		} else {
			itemMissingNames[itemID] = ""
		}
	}

	if len(itemMissingNames) > 0 {
		snIds := make([]int32, 0, len(itemMissingNames))
		for itemID := range itemMissingNames {
			snIds = append(snIds, itemID)
		}

		sncallParam := ESIUniverse.NewPostUniverseNamesParams()
		sncallParam.WithIds(snIds)

		skillNameResp, skillNameErr := swaggerclient.Universe.PostUniverseNames(sncallParam)
		if skillNameErr != nil {
			log.Printf("Error on PostUniverseNames on items: %v\n", snIds)
			log.Printf("Error on PostUniverseNames: %s\n", skillNameErr)
		} else {
			for _, searchResult := range skillNameResp.Payload {
				itemName := searchResult.Name
				itemID := searchResult.ID

				cache.PutCacheDataSub(*itemID, "name", *itemName)

				itemNames[*itemID] = *itemName
			}
		}
	}

	return itemNames
}

type planetInformation struct {
	PlanetName   string
	PlanetTypeID int32
}

func getPlanetInformation(swaggerclient *ESI.App, planetID int32) planetInformation {

	planetName := cache.GetCachedDataSub(planetID, "name")
	planetType := cache.GetCachedDataIntSub(planetID, "type")

	if planetName == "" {
		pcallParams := ESIUniverse.NewGetUniversePlanetsPlanetIDParams()
		pcallParams.WithPlanetID(planetID)

		pesiresponse, pesierror := swaggerclient.Universe.GetUniversePlanetsPlanetID(pcallParams)
		if pesierror != nil {
			log.Printf("Error on GetUniversePlanetsPlanetID on planet: %v\n", planetID)
			log.Printf("Error on GetUniversePlanetsPlanetID: %s\n", pesierror)
		}

		planetESIInfo := pesiresponse.Payload

		var planetInfo planetInformation
		planetInfo.PlanetName = *planetESIInfo.Name
		planetInfo.PlanetTypeID = *planetESIInfo.TypeID

		cache.PutCacheDataSub(planetID, "name", planetInfo.PlanetName)
		cache.PutCacheDataIntSub(planetID, "type", planetInfo.PlanetTypeID)

		return planetInfo
	}

	var planetInfo planetInformation
	planetInfo.PlanetName = planetName
	planetInfo.PlanetTypeID = planetType

	return planetInfo
}

type solarSystemInfo struct {
	SolarSystemName   string
	ConstellationID   int32
	ConstellationName string
	RegionID          int32
	RegionName        string
}

func getSolarSystemInformation(swaggerclient *ESI.App, solarSystemID int32) solarSystemInfo {

	systemName := cache.GetCachedDataSub(solarSystemID, "name")
	systemConstellationID := cache.GetCachedDataIntSub(solarSystemID, "constellation")
	constellationName := cache.GetCachedDataSub(systemConstellationID, "name")
	constellationRegionID := cache.GetCachedDataIntSub(systemConstellationID, "region")
	regionName := cache.GetCachedDataSub(constellationRegionID, "name")

	if systemName == "" || constellationName == "" || regionName == "" {
		scallParams := ESIUniverse.NewGetUniverseSystemsSystemIDParams()
		scallParams.WithSystemID(solarSystemID)

		sesiresponse, sesierror := swaggerclient.Universe.GetUniverseSystemsSystemID(scallParams)
		if sesierror != nil {
			log.Printf("Error on GetUniverseSystemsSystemID on system: %v\n", solarSystemID)
			log.Printf("Error on GetUniverseSystemsSystemID: %s\n", sesierror)
		}

		solarSystemESIInfo := sesiresponse.Payload

		var ssInfo solarSystemInfo
		ssInfo.SolarSystemName = *solarSystemESIInfo.Name
		ssInfo.ConstellationID = *solarSystemESIInfo.ConstellationID

		ccallParams := ESIUniverse.NewGetUniverseConstellationsConstellationIDParams()
		ccallParams.WithConstellationID(ssInfo.ConstellationID)

		cesiresponse, cesierror := swaggerclient.Universe.GetUniverseConstellationsConstellationID(ccallParams)
		if cesierror != nil {
			log.Printf("Error on GetUniverseConstellationsConstellationID on constellation: %v\n", ssInfo.ConstellationID)
			log.Printf("Error on GetUniverseConstellationsConstellationID: %s\n", cesierror)
		}

		constellationESIInfo := cesiresponse.Payload

		ssInfo.ConstellationName = *constellationESIInfo.Name
		ssInfo.RegionID = *constellationESIInfo.RegionID

		rcallParams := ESIUniverse.NewGetUniverseRegionsRegionIDParams()
		rcallParams.WithRegionID(ssInfo.RegionID)

		resiresponse, resierror := swaggerclient.Universe.GetUniverseRegionsRegionID(rcallParams)
		if resierror != nil {
			log.Printf("Error on GetUniverseRegionsRegionID on region: %v\n", ssInfo.RegionID)
			log.Printf("Error on GetUniverseRegionsRegionID: %s\n", cesierror)
		}

		regionESIInfo := resiresponse.Payload

		ssInfo.RegionName = *regionESIInfo.Name

		cache.PutCacheDataSub(solarSystemID, "name", ssInfo.SolarSystemName)
		cache.PutCacheDataIntSub(solarSystemID, "constellation", ssInfo.ConstellationID)

		cache.PutCacheDataSub(ssInfo.ConstellationID, "name", ssInfo.ConstellationName)
		cache.PutCacheDataIntSub(ssInfo.ConstellationID, "region", ssInfo.RegionID)

		cache.PutCacheDataSub(ssInfo.RegionID, "name", ssInfo.RegionName)

		return ssInfo
	}

	var ssInfo solarSystemInfo

	ssInfo.SolarSystemName = systemName
	ssInfo.ConstellationID = systemConstellationID
	ssInfo.ConstellationName = constellationName
	ssInfo.RegionID = constellationRegionID
	ssInfo.RegionName = regionName
	return ssInfo
}

// SchematicInfo - Structure to store and cache the schematics information from ESI
type SchematicInfo struct {
	CycleTime     int32
	SchematicName string
}

func getSchematicsInformation(swaggerclient *ESI.App, schematicID int32) (*SchematicInfo, error) {

	schematicName := cache.GetCachedDataSub(schematicID, "name")
	schematicCycle := cache.GetCachedDataIntSub(schematicID, "cycle")

	if schematicName == "" {
		scallParams := ESIPlanetaryInteraction.NewGetUniverseSchematicsSchematicIDParams()
		scallParams.WithSchematicID(schematicID)

		sesiresponse, sesierr := swaggerclient.PlanetaryInteraction.GetUniverseSchematicsSchematicID(scallParams)
		if sesierr != nil {
			return nil, sesierr
		}

		schematicsESIInfo := sesiresponse.Payload

		var schematicInfo SchematicInfo
		schematicInfo.CycleTime = *schematicsESIInfo.CycleTime
		schematicInfo.SchematicName = *schematicsESIInfo.SchematicName

		cache.PutCacheDataSub(schematicID, "name", schematicInfo.SchematicName)
		cache.PutCacheDataIntSub(schematicID, "cycle", schematicInfo.CycleTime)

		return &schematicInfo, nil
	}

	var schematicInfo SchematicInfo
	schematicInfo.CycleTime = schematicCycle
	schematicInfo.SchematicName = schematicName

	return &schematicInfo, nil
}

func getNewAuthorizationToken(httpConfiguration *InternalUtils.HTTPConfiguration) *oauth2.Token {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { handleLogin(w, r, httpConfiguration) })
	http.HandleFunc("/callback", handleAuthenticationCallback)

	go func() {
		fmt.Println("No available token. Please visit http://localhost:3000 to renew.")
		http.ListenAndServe(":3000", nil)
	}()

	return <-messages
}

func handleLogin(w http.ResponseWriter, r *http.Request, httpConfiguration *InternalUtils.HTTPConfiguration) {
	url := InternalUtils.GetTokenURL(httpConfiguration)
	log.Printf("URL will be %s\n", url)
	http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

func handleAuthenticationCallback(w http.ResponseWriter, r *http.Request) {
	client, token, clientErr := InternalUtils.GetTemporaryClient(r)
	if clientErr != nil {
		log.Printf("Code exchange failed with '%s'\n", clientErr)
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		// No token to pass, we will get one on the second pass
		return
	}

	charInfo, charErr := getCharacterInfo(client)
	if charErr != nil {
		fmt.Printf("Character read error with '%s'\n", charErr)
		http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
		return
	}

	fmt.Fprintf(w, "Got token for character %s.\n", charInfo.CharacterName)
	fmt.Fprintf(w, "You can now close this navigator tab.\n")

	cache.PutCacheData("accessToken", token.AccessToken)
	cache.PutCacheData("refreshToken", token.RefreshToken)

	messages <- token
}

func getDatabaseToken() *oauth2.Token {
	refreshToken := cache.GetCachedData("refreshToken")
	//accessToken := cache.GetCachedData("accessToken")

	token := new(oauth2.Token)
	token.RefreshToken = refreshToken
	//token.AccessToken = accessToken
	token.TokenType = "Bearer"

	return token
}
