package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"runtime"

	"go.uber.org/atomic"

	"git.xeserv.us/xena/route/internal/database"
	"git.xeserv.us/xena/route/internal/routecrypto"
	proto "git.xeserv.us/xena/route/proto"
	"git.xeserv.us/xena/route/proto/route"
	"github.com/Xe/ln"
	"github.com/Xe/uuid"
	"github.com/dickeyxxx/netrc"
	"github.com/kr/pretty"
	"github.com/olekukonko/tablewriter"
	"gopkg.in/alecthomas/kingpin.v2"
)

var allScopes = []string{
	"token:get", "token:getall", "token:put", "token:delete", "token:deactivate",
	"route:get", "route:getall", "route:put", "route:delete",
	"connect",
	"backend:list", "backend:kill",
	"admin",
}

var whoami string

func userHomeDir() string {
	if runtime.GOOS == "windows" {
		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
		if home == "" {
			home = os.Getenv("USERPROFILE")
		}
		return home
	}
	return os.Getenv("HOME")
}

var (
	app        = kingpin.New("route", "An interface to routed")
	grpcServer = app.Flag("routed-addr", "routed grpc address").Default("127.0.0.1:7268").String()
	netrcPath  = app.Flag("netrc", "netrc path").Default(filepath.Join(userHomeDir(), ".netrc")).String()

	generateKey = app.Command("generate-key", "generate SSL cert crypto key")

	backends = app.Command("backend", "backend management")

	backendList       = backends.Command("list", "list connected backends")
	backendListDomain = backendList.Flag("domain", "if set, match backends against this domain name").String()
	backendListUser   = backendList.Flag("user", "if set, match backends against this username").String()

	backendKill   = backends.Command("kill", "forcibly disconnect a given backend")
	backendKillID = backendKill.Flag("id", "the ID of the backend to remove").Required().String()

	routes = app.Command("route", "route management")

	routesCreate       = routes.Command("create", "create a new route")
	routesCreateDomain = routesCreate.Flag("domain", "domain for the route (if not given one will be generated for you)").String()

	routesInspect   = routes.Command("inspect", "inspect one route")
	routesInspectID = routesInspect.Flag("id", "ID of the route to inspect").Required().String()

	routesList = routes.Command("list", "list all routes owned by you")

	routesRm   = routes.Command("rm", "remove a route")
	routesRmID = routesRm.Flag("id", "route ID to remove").Required().String()

	testServer     = app.Command("test-server", "spawn a simple HTTP test server on a TCP address")
	testServerAddr = testServer.Flag("addr", "TCP address").Default(":9090").String()

	token = app.Command("token", "token management")

	tokenCreate       = token.Command("create", "create a new token")
	tokenCreateScopes = tokenCreate.Flag("scope", "token scopes").Default(allScopes...).Strings()

	tokenGenerate             = token.Command("generate-root", "generate a root token")
	tokenGenerateKey          = tokenGenerate.Flag("key", "token crypto key").Required().String()
	tokenGenerateScopes       = tokenGenerate.Flag("scopes", "token scopes").Default(allScopes...).Strings()
	tokenGenerateUsername     = tokenGenerate.Flag("username", "token username").Required().String()
	tokenGenerateDatabasePath = tokenGenerate.Flag("db", "database file to add the root token to").Required().String()

	tokenInspect   = token.Command("inspect", "inspect a token")
	tokenInspectID = tokenInspect.Flag("token-id", "token id").Required().String()

	tokenList            = token.Command("list", "list all tokens belonging to you")
	tokenListDeactivated = tokenList.Flag("deactivated", "list deactivated tokens?").Default("false").Bool()

	tokenRm     = token.Command("rm", "remove a token")
	tokenRmHard = tokenRm.Flag("hard", "hard-delete the token instead of deactivating it").Default("false").Bool()
	tokenRmID   = tokenRm.Flag("id", "token id").Required().String()
)

var (
	hits *atomic.Int64
)

func init() {
	hits = atomic.NewInt64(0)
}

func handle(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Route is go!")
	fmt.Fprintf(w, "%s\n", pretty.Sprintf("%s", r.Header))
	hn, _ := os.Hostname()
	fmt.Fprintf(w, "Served by %s running %s\n", hn, runtime.GOOS)
	fmt.Fprintf(w, "Hit count: %d", hits.Inc())

	ip := r.Header.Get("X-Remote-Ip")
	if ip != "" {
		log.Printf("Hit from %s: %s", ip, r.RequestURI)
	}
}

func main() {
	cmdline := kingpin.MustParse(app.Parse(os.Args[1:]))
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

retry_netrc:
	n, err := netrc.Parse(*netrcPath)
	if err != nil {
		_, err := os.Stat(*netrcPath)
		if err == os.ErrNotExist {
			fout, err := os.Create(*netrcPath)
			if err != nil {
				ln.FatalErr(ctx, err, ln.Action("creating netrc"), ln.F{"path": *netrcPath})
			}
			fout.Close()
			goto retry_netrc
		}
	}
	_ = n

	switch cmdline {
	case "test-server":
		http.HandleFunc("/", handle)

		ln.FatalErr(ctx, http.ListenAndServe(*testServerAddr, nil), ln.Action("test server listenAndServe"))

		return

	case "generate-key":
		key, err := routecrypto.GenerateKey()
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("generating encryption key"))
		}

		fmt.Println("Your key is:", routecrypto.ShowKey(key))

		return

	case "token generate-root":
		key, err := routecrypto.ParseKey(*tokenGenerateKey)
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("parsing encryption key"))
		}
		db, err := database.NewBoltStorage(*tokenGenerateDatabasePath, key)
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("opening routed database"))
		}

		tBody := uuid.New()

		tok := database.Token{
			Body:   uuid.New(),
			Owner:  *tokenGenerateUsername,
			Scopes: *tokenGenerateScopes,
		}

		_, err = db.Tokens().Put(context.Background(), tok)
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("add newly created token to database"))
		}
		defer db.Close()

		fmt.Println("Your token is:", tBody)

		n.AddMachine(*grpcServer, *tokenGenerateUsername, tBody)
		err = n.Save()
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("add machine to netrc"))
		}

		return
	}

	m := n.Machine(*grpcServer)

	cl := route.New("https://"+*grpcServer, m.Get("password"), &http.Client{})

	switch cmdline {
	case "route create":
		rt, err := cl.Routes.Put(ctx, &proto.Route{Host: *routesCreateDomain})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("create new route"))
		}

		fmt.Printf("created route for domain %s, id: %s\n", rt.Host, rt.Id)

		return

	case "route inspect":
		r, err := cl.Routes.Get(context.Background(), &proto.GetRouteRequest{
			Id: *routesInspectID,
		})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("get single route"), ln.F{"domain": *routesCreateDomain})
		}

		json.NewEncoder(os.Stdout).Encode(r)
		fmt.Println()

		return

	case "route list":
		rts, err := cl.Routes.GetAll(context.Background(), &proto.Nil{})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("get all routes"))
		}

		table := tablewriter.NewWriter(os.Stdout)
		table.SetHeader([]string{"ID", "Host"})

		for _, r := range rts.Routes {
			table.Append([]string{r.Id, r.Host})
		}

		table.Render()

		return

	case "route rm":
		_, err := cl.Routes.Delete(context.Background(), &proto.Route{Id: *routesRmID})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("remove single route"), ln.F{"id": *routesRmID})
		}

		return

	case "backend list":
		bkds, err := cl.Backends.List(context.Background(), &proto.BackendSelector{
			Domain: *backendListDomain,
			User:   *backendListUser,
		})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("list backends"))
		}

		table := tablewriter.NewWriter(os.Stdout)
		table.SetHeader([]string{"ID", "Proto", "User", "Domain", "Failure Chance", "Host"})

		for _, bknd := range bkds.Backends {
			table.Append([]string{bknd.Id, bknd.Proto, bknd.User, bknd.Domain, fmt.Sprint(bknd.Phi), bknd.Host})
		}

		table.Render()

		return

	case "backend kill":
		_, err := cl.Backends.Kill(context.Background(), &proto.BackendID{Id: *backendKillID})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("attempt to kill backend"), ln.F{"backend_id": *backendKillID})
		}

		fmt.Println("killed backend " + *backendKillID)

		return

	case "token list":
		lis, err := cl.Tokens.GetAll(ctx, &proto.Nil{})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("get all tokens"))
		}

		table := tablewriter.NewWriter(os.Stdout)
		table.SetHeader([]string{"ID", "Active", "Scopes"})

		for _, tkn := range lis.Tokens {
			table.Append([]string{tkn.Id, fmt.Sprint(tkn.Active), fmt.Sprint(tkn.Scopes)})
		}

		table.Render()

		return

	case "token create":
		scps := *tokenCreateScopes
		tkn := &proto.Token{
			Scopes: scps,
		}

		ftkn, err := cl.Tokens.Put(ctx, tkn)
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("put token to server"))
		}

		fmt.Printf("Your token is: %s\n", ftkn.Body)
		fmt.Printf("It has permission for the following scopes: %v\n", ftkn.Scopes)

		return

	case "token inspect":
		tkn, err := cl.Tokens.Get(ctx, &proto.GetTokenRequest{Id: *tokenInspectID})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("fetch token from server"), ln.F{"token_id": *tokenInspectID})
		}

		json.NewEncoder(os.Stdout).Encode(tkn)

		return

	case "token rm":
		tkn, err := cl.Tokens.Get(ctx, &proto.GetTokenRequest{Id: *tokenRmID})
		if err != nil {
			ln.FatalErr(ctx, err, ln.Action("fetch token from server"), ln.F{"token_id": *tokenRmID})
		}

		var action ln.Fer
		if *tokenRmHard {
			_, err = cl.Tokens.Delete(ctx, tkn)
			action = ln.Action("actually delete token")
		} else {
			_, err = cl.Tokens.Deactivate(ctx, tkn)
			action = ln.Action("deactivate token")
		}

		if err != nil {
			ln.FatalErr(ctx, err, ln.F{"token_id": *tokenRmID}, action)
		}

		fmt.Printf("token with id %s and body %s removed.\n", tkn.Id, tkn.Body)

		return
	}

	ln.Fatal(ctx, ln.Action("not implemented"), ln.F{"command": cmdline})
}
