package main

import (
	"bufio"
	"context"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/ddworken/hishtory/client/data"
	"github.com/ddworken/hishtory/client/hctx"
	"github.com/ddworken/hishtory/client/lib"
)

var GitCommit string = "Unknown"

func main() {
	if len(os.Args) == 1 {
		fmt.Println("Must specify a command! Do you mean `hishtory query`?")
		return
	}
	switch os.Args[1] {
	case "saveHistoryEntry":
		ctx := hctx.MakeContext()
		saveHistoryEntry(ctx)
	case "query":
		ctx := hctx.MakeContext()
		lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
		query(ctx, strings.Join(os.Args[2:], " "))
	case "tquery":
		ctx := hctx.MakeContext()
		lib.CheckFatalError(lib.TuiQuery(ctx, GitCommit, strings.Join(os.Args[2:], " ")))
	case "export":
		ctx := hctx.MakeContext()
		lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
		export(ctx, strings.Join(os.Args[2:], " "))
	case "redact":
		fallthrough
	case "delete":
		ctx := hctx.MakeContext()
		lib.CheckFatalError(lib.RetrieveAdditionalEntriesFromRemote(ctx))
		lib.CheckFatalError(lib.ProcessDeletionRequests(ctx))
		query := strings.Join(os.Args[2:], " ")
		force := false
		if os.Args[2] == "--force" {
			query = strings.Join(os.Args[3:], " ")
			force = true
		}
		lib.CheckFatalError(lib.Redact(ctx, query, force))
	case "init":
		db, err := hctx.OpenLocalSqliteDb()
		lib.CheckFatalError(err)
		data, err := lib.Search(nil, db, "", 10)
		lib.CheckFatalError(err)
		if len(data) > 0 {
			fmt.Printf("Your current hishtory profile has saved history entries, are you sure you want to run `init` and reset?\nNote: This won't clear any imported history entries from your existing shell\n[y/N]")
			reader := bufio.NewReader(os.Stdin)
			resp, err := reader.ReadString('\n')
			lib.CheckFatalError(err)
			if strings.TrimSpace(resp) != "y" {
				fmt.Printf("Aborting init per user response of %#v\n", strings.TrimSpace(resp))
				return
			}
		}
		lib.CheckFatalError(lib.Setup(os.Args))
		if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
			fmt.Println("Importing existing shell history...")
			ctx := hctx.MakeContext()
			numImported, err := lib.ImportHistory(ctx, false, false)
			lib.CheckFatalError(err)
			if numImported > 0 {
				fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
			}
		}
	case "install":
		lib.CheckFatalError(lib.Install())
		if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
			db, err := hctx.OpenLocalSqliteDb()
			lib.CheckFatalError(err)
			data, err := lib.Search(nil, db, "", 10)
			lib.CheckFatalError(err)
			if len(data) < 10 {
				fmt.Println("Importing existing shell history...")
				ctx := hctx.MakeContext()
				numImported, err := lib.ImportHistory(ctx, false, false)
				lib.CheckFatalError(err)
				if numImported > 0 {
					fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
				}
			}
		}
	case "uninstall":
		fmt.Printf("Are you sure you want to uninstall hiSHtory and delete all locally saved history data [y/N]")
		reader := bufio.NewReader(os.Stdin)
		resp, err := reader.ReadString('\n')
		lib.CheckFatalError(err)
		if strings.TrimSpace(resp) != "y" {
			fmt.Printf("Aborting uninstall per user response of %#v\n", strings.TrimSpace(resp))
			return
		}
		lib.CheckFatalError(lib.Uninstall(hctx.MakeContext()))
	case "import":
		ctx := hctx.MakeContext()
		numImported, err := lib.ImportHistory(ctx, true, true)
		lib.CheckFatalError(err)
		if numImported > 0 {
			fmt.Printf("Imported %v history entries from your existing shell history\n", numImported)
		}
	case "enable":
		ctx := hctx.MakeContext()
		lib.CheckFatalError(lib.Enable(ctx))
	case "disable":
		ctx := hctx.MakeContext()
		lib.CheckFatalError(lib.Disable(ctx))
	case "version":
		fallthrough
	case "status":
		ctx := hctx.MakeContext()
		config := hctx.GetConf(ctx)
		fmt.Printf("hiSHtory: v0.%s\nEnabled: %v\n", lib.Version, config.IsEnabled)
		fmt.Printf("Secret Key: %s\n", config.UserSecret)
		if len(os.Args) == 3 && os.Args[2] == "-v" {
			fmt.Printf("User ID: %s\n", data.UserId(config.UserSecret))
			fmt.Printf("Device ID: %s\n", config.DeviceId)
			printDumpStatus(config)
		}
		fmt.Printf("Commit Hash: %s\n", GitCommit)
	case "update":
		err := lib.Update(hctx.MakeContext())
		if err != nil {
			log.Fatalf("Failed to update hishtory: %v", err)
		}
	case "config-get":
		ctx := hctx.MakeContext()
		config := hctx.GetConf(ctx)
		key := os.Args[2]
		switch key {
		case "enable-control-r":
			fmt.Printf("%v", config.ControlRSearchEnabled)
		case "filter-duplicate-commands":
			fmt.Printf("%v", config.FilterDuplicateCommands)
		case "displayed-columns":
			for _, col := range config.DisplayedColumns {
				if strings.Contains(col, " ") {
					fmt.Printf("%q ", col)
				} else {
					fmt.Print(col + " ")
				}
			}
			fmt.Print("\n")
		case "custom-columns":
			for _, cc := range config.CustomColumns {
				fmt.Println(cc.ColumnName + ":   " + cc.ColumnCommand)
			}
		default:
			log.Fatalf("Unrecognized config key: %s", key)
		}
	case "config-set":
		ctx := hctx.MakeContext()
		config := hctx.GetConf(ctx)
		key := os.Args[2]
		switch key {
		case "enable-control-r":
			val := os.Args[3]
			if val != "true" && val != "false" {
				log.Fatalf("Unexpected config value %s, must be one of: true, false", val)
			}
			config.ControlRSearchEnabled = (val == "true")
			lib.CheckFatalError(hctx.SetConfig(config))
		case "filter-duplicate-commands":
			val := os.Args[3]
			if val != "true" && val != "false" {
				log.Fatalf("Unexpected config value %s, must be one of: true, false", val)
			}
			config.FilterDuplicateCommands = (val == "true")
			lib.CheckFatalError(hctx.SetConfig(config))
		case "displayed-columns":
			vals := os.Args[3:]
			config.DisplayedColumns = vals
			lib.CheckFatalError(hctx.SetConfig(config))
		case "timestamp-format":
			val := os.Args[3]
			config.TimestampFormat = val
			lib.CheckFatalError(hctx.SetConfig(config))
		case "custom-columns":
			log.Fatalf("Please use config-add and config-delete to interact with custom-columns")
		default:
			log.Fatalf("Unrecognized config key: %s", key)
		}
	case "config-add":
		ctx := hctx.MakeContext()
		config := hctx.GetConf(ctx)
		key := os.Args[2]
		switch key {
		case "custom-column":
			fallthrough
		case "custom-columns":
			columnName := os.Args[3]
			command := os.Args[4]
			ctx := hctx.MakeContext()
			config := hctx.GetConf(ctx)
			if config.CustomColumns == nil {
				config.CustomColumns = make([]hctx.CustomColumnDefinition, 0)
			}
			config.CustomColumns = append(config.CustomColumns, hctx.CustomColumnDefinition{ColumnName: columnName, ColumnCommand: command})
			lib.CheckFatalError(hctx.SetConfig(config))
		case "displayed-columns":
			vals := os.Args[3:]
			config.DisplayedColumns = append(config.DisplayedColumns, vals...)
			lib.CheckFatalError(hctx.SetConfig(config))
		default:
			log.Fatalf("Unrecognized config key: %s", key)
		}
	case "config-delete":
		ctx := hctx.MakeContext()
		config := hctx.GetConf(ctx)
		key := os.Args[2]
		switch key {
		case "custom-columns":
			columnName := os.Args[2]
			ctx := hctx.MakeContext()
			config := hctx.GetConf(ctx)
			if config.CustomColumns == nil {
				return
			}
			newColumns := make([]hctx.CustomColumnDefinition, 0)
			deletedColumns := false
			for _, c := range config.CustomColumns {
				if c.ColumnName != columnName {
					newColumns = append(newColumns, c)
					deletedColumns = true
				}
			}
			if !deletedColumns {
				log.Fatalf("Did not find a column with name %#v to delete (current columns = %#v)", columnName, config.CustomColumns)
			}
			config.CustomColumns = newColumns
			lib.CheckFatalError(hctx.SetConfig(config))
		case "displayed-columns":
			deletedColumns := os.Args[3:]
			newColumns := make([]string, 0)
			for _, c := range config.DisplayedColumns {
				isDeleted := false
				for _, d := range deletedColumns {
					if c == d {
						isDeleted = true
					}
				}
				if !isDeleted {
					newColumns = append(newColumns, c)
				}
			}
			config.DisplayedColumns = newColumns
			lib.CheckFatalError(hctx.SetConfig(config))
		default:
			log.Fatalf("Unrecognized config key: %s", key)
		}
	case "reupload":
		// Purposefully undocumented since this command is generally not necessary to run
		lib.CheckFatalError(lib.Reupload(hctx.MakeContext()))
	case "-h":
		fallthrough
	case "help":
		fmt.Print(`hiSHtory: Better shell history

Supported commands:
    'hishtory query': Query for matching commands and display them in a table. Examples:
		'hishtory query apt-get'  			# Find shell commands containing 'apt-get'
		'hishtory query apt-get install'  	# Find shell commands containing 'apt-get' and 'install'
		'hishtory query curl cwd:/tmp/'  	# Find shell commands containing 'curl' run in '/tmp/'
		'hishtory query curl user:david'	# Find shell commands containing 'curl' run by 'david'
		'hishtory query curl host:x1'		# Find shell commands containing 'curl' run on 'x1'
		'hishtory query exit_code:1'		# Find shell commands that exited with status code 1
		'hishtory query before:2022-02-01'	# Find shell commands run before 2022-02-01
	'hishtory export': Query for matching commands and display them in list without any other 
		metadata. Supports the same query format as 'hishtory query'. 
	'hishtory redact': Query for matching commands and remove them from your shell history (on the
		current machine and on all remote machines). Supports the same query format as 'hishtory query'.
	'hishtory update': Securely update hishtory to the latest version. 
	'hishtory disable': Stop recording shell commands 
	'hishtory enable': Start recording shell commands 
	'hishtory status': View status info including the secret key which is needed to sync shell
		history from another machine. 
	'hishtory init': Set the secret key to enable syncing shell commands from another 
		machine with a matching secret key. 
	'hishtory config-get', 'hishtory config-set', 'hishtory config-add', 'hishtory config-delete': Edit the config. See the README for details on each of the config options.
	'hishtory uninstall': Permanently uninstall hishtory
	'hishtory help': View this help page
		`)
	default:
		lib.CheckFatalError(fmt.Errorf("unknown command: %s", os.Args[1]))
	}
}

func printDumpStatus(config hctx.ClientConfig) {
	fmt.Print("\n")
}

func query(ctx *context.Context, query string) {
	db := hctx.GetDb(ctx)
	err := lib.RetrieveAdditionalEntriesFromRemote(ctx)
	if err != nil {
		if lib.IsOfflineError(err) {
			fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!")
		} else {
			lib.CheckFatalError(err)
		}
	}
	lib.CheckFatalError(displayBannerIfSet(ctx))
	numResults := 25
	data, err := lib.Search(ctx, db, query, numResults*5)
	lib.CheckFatalError(err)
	lib.CheckFatalError(lib.DisplayResults(ctx, data, numResults))
}

func displayBannerIfSet(ctx *context.Context) error {
	respBody, err := lib.GetBanner(ctx, GitCommit)
	if lib.IsOfflineError(err) {
		return nil
	}
	if err != nil {
		return err
	}
	if len(respBody) > 0 {
		fmt.Println(string(respBody))
	}
	return nil
}

func saveHistoryEntry(ctx *context.Context) {
	config := hctx.GetConf(ctx)
	if !config.IsEnabled {
		hctx.GetLogger().Infof("Skipping saving a history entry because hishtory is disabled\n")
		return
	}
	entry, err := lib.BuildHistoryEntry(ctx, os.Args)
	lib.CheckFatalError(err)
	if entry == nil {
		hctx.GetLogger().Infof("Skipping saving a history entry because we did not build a history entry (was the command prefixed with a space and/or empty?)\n")
		return
	}

	// Persist it locally
	db := hctx.GetDb(ctx)
	err = lib.ReliableDbCreate(db, *entry)
	lib.CheckFatalError(err)

	// Persist it remotely
	if !config.IsOffline {
		jsonValue, err := lib.EncryptAndMarshal(config, []*data.HistoryEntry{entry})
		lib.CheckFatalError(err)
		_, err = lib.ApiPost("/api/v1/submit?source_device_id="+config.DeviceId, "application/json", jsonValue)
		if err != nil {
			if lib.IsOfflineError(err) {
				hctx.GetLogger().Infof("Failed to remotely persist hishtory entry because we failed to connect to the remote server! This is likely because the device is offline, but also could be because the remote server is having reliability issues. Original error: %v", err)
				if !config.HaveMissedUploads {
					config.HaveMissedUploads = true
					config.MissedUploadTimestamp = time.Now().Unix()
					lib.CheckFatalError(hctx.SetConfig(config))
				}
			} else {
				lib.CheckFatalError(err)
			}
		}
	}

}

func export(ctx *context.Context, query string) {
	db := hctx.GetDb(ctx)
	err := lib.RetrieveAdditionalEntriesFromRemote(ctx)
	if err != nil {
		if lib.IsOfflineError(err) {
			fmt.Println("Warning: hishtory is offline so this may be missing recent results from your other machines!")
		} else {
			lib.CheckFatalError(err)
		}
	}
	data, err := lib.Search(ctx, db, query, 0)
	lib.CheckFatalError(err)
	for i := len(data) - 1; i >= 0; i-- {
		fmt.Println(data[i].Command)
	}
}

// TODO(feature): Add a session_id column that corresponds to the shell session the command was run in
