// Copyright (C) 2023 Marius Schellenberger

package cli

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"
	"text/tabwriter"

	"git.giftfish.de/ston1th/godrop/v2"
	apiclient "git.giftfish.de/ston1th/keyctl/pkg/api/client"
	clientv1 "git.giftfish.de/ston1th/keyctl/pkg/api/v1/client"
	"git.giftfish.de/ston1th/keyctl/pkg/api/v1/schema"
	"git.giftfish.de/ston1th/keyctl/pkg/core"
)

var fs = flag.NewFlagSet("", flag.ExitOnError)

func exit(err error) {
	fmt.Fprintf(os.Stderr, "%s\n", err)
	os.Exit(1)
}

func Run(arg string) {
	ctx := context.Background()
	pledge := "stdio unveil"
	switch arg {
	case "new", "ls", "req", "approve", "reject", "del":
		pledge += " unix"
	case "get":
		pledge += " unix inet dns"
	}
	err := godrop.PledgePromises(pledge)
	if err != nil {
		exit(err)
	}
	switch arg {
	case "new":
		name, existing, encoding, t, size, err := newKeyFlags()
		if err != nil {
			exit(err)
		}
		newKey := schema.NewKey{
			Name:     name,
			Existing: existing,
			Size:     size,
			Encoding: core.EncodingFromString(encoding),
			Type:     core.TypeFromString(t),
		}
		c := client()
		k, err := c.CreateKey(ctx, newKey)
		if err != nil {
			exit(err)
		}
		fmt.Println("Key ID:", k.ID)
		if k.Type == core.Shamir || k.Type == core.Xor {
			fmt.Println("Key Type:", k.Type)
			fmt.Println("Key Share:", k.Key)
		}
	case "del":
		id, force, err := delFlags("key id")
		if err != nil {
			exit(err)
		}
		if !force && !confirm(os.Stdin) {
			exit(errors.New("confirmation failed: secret not deleted"))
		}
		c := client()
		err = c.DeleteKey(ctx, id)
		if err != nil {
			exit(err)
		}
	case "approve":
		id, err := idFlags("approval id")
		if err != nil {
			exit(err)
		}
		c := client()
		err = c.AcceptApproval(ctx, id)
		if err != nil {
			exit(err)
		}
	case "reject":
		id, err := idFlags("approval id")
		if err != nil {
			exit(err)
		}
		c := client()
		err = c.RejectApproval(ctx, id)
		if err != nil {
			exit(err)
		}
	case "get":
		id, share, loop, json, newline, err := getFlags()
		if err != nil {
			exit(err)
		}
		c := client()
		var key schema.Key
		if loop {
			key, err = c.GetKeyLoop(ctx, id)
			if err != nil {
				exit(err)
			}
		} else {
			key, err = c.GetKey(ctx, id)
			if err != nil {
				exit(err)
			}
		}
		out, err := key.Print(share)
		if err != nil {
			exit(err)
		}
		if json {
			key.Key = out
			jsonOutput(key) // does not return
		}
		if newline {
			fmt.Println(out)
		} else {
			fmt.Print(out)
		}
	case "ls":
		json := outputFlags()
		c := client()
		keys, err := c.GetKeys(ctx)
		if err != nil {
			exit(err)
		}
		if json {
			jsonOutput(keys) // does not return
		}
		print(keys)
	case "req":
		json := outputFlags()
		c := client()
		approvals, err := c.GetApprovals(ctx)
		if err != nil {
			exit(err)
		}
		if json {
			jsonOutput(approvals) // does not return
		}
		print(approvals)
	}
	os.Exit(0)
}

func client() (c *clientv1.Client) {
	opts, err := apiclient.ClientOptionsFromEnv()
	if err != nil {
		exit(err)
	}
	apic := apiclient.NewClient(opts...)
	if socket, ok := apic.IsUnix(); ok {
		err = unveil(socket)
		if err != nil {
			exit(err)
		}
	}
	c, err = clientv1.NewClient(apic)
	if err != nil {
		exit(err)
	}
	return
}

func unveil(socket string) error {
	err := godrop.Unveil(socket, "rw")
	if err != nil {
		return err
	}
	return godrop.UnveilBlock()
}

func jsonOutput(v any) {
	b, err := json.Marshal(v)
	if err != nil {
		exit(err)
	}
	fmt.Println(string(b))
	os.Exit(0)
}

func idFlags(desc string) (id string, err error) {
	fs.StringVar(&id, "id", "", desc)
	fs.Parse(os.Args[2:])
	if id == "" {
		err = errors.New("missing flag: id")
	}
	return
}

func delFlags(desc string) (id string, force bool, err error) {
	fs.StringVar(&id, "id", "", desc)
	fs.BoolVar(&force, "force", false, "do not ask for confirmation")
	fs.Parse(os.Args[2:])
	if id == "" {
		err = errors.New("missing flag: id")
	}
	return
}

func getFlags() (id, share string, loop, json, newline bool, err error) {
	fs.StringVar(&id, "id", "", "key id")
	fs.StringVar(&share, "share", "", "shamir key share")
	fs.BoolVar(&loop, "loop", false, "ignore network errors and loop until key is received")
	fs.BoolVar(&json, "json", false, "json output")
	fs.BoolVar(&newline, "newline", true, "output newline after secret")
	fs.Parse(os.Args[2:])
	if id == "" {
		err = errors.New("missing flag: id")
	}
	if share == "" {
		share = os.Getenv("KEYCTL_SHARE")
	}
	return
}

func outputFlags() (json bool) {
	fs.BoolVar(&json, "json", false, "json output")
	fs.Parse(os.Args[2:])
	return
}

func newKeyFlags() (name, existing, encoding, t string, size int, err error) {
	fs.StringVar(&name, "name", "", "key name")
	fs.StringVar(&existing, "key", "", "use existing secret key (optional; input-format: hex; types: plain, shamir)")
	fs.StringVar(&encoding, "enc", "hex", "key encoding scheme [hex|base32|base64|base64url]")
	fs.StringVar(&t, "type", "plain", "key type [plain|shamir|xor]")
	fs.IntVar(&size, "size", 16, "key size in bytes")
	fs.Parse(os.Args[2:])
	if name == "" {
		err = errors.New("missing flag: name")
	}
	return
}

func print(tw core.TableWriter) {
	if tw.Len() == 0 {
		return
	}
	w := tabwriter.NewWriter(os.Stdout, 1, 0, 1, ' ', tabwriter.Debug)
	fmt.Fprintln(w, tw.Fields())
	for _, v := range tw.Values() {
		fmt.Fprintln(w, v)
	}
	w.Flush()
}

func confirm(r io.Reader) (ok bool) {
	br := bufio.NewReader(r)
	fmt.Printf("confirm delete [y]: ")
	res, err := br.ReadString('\n')
	if err != nil {
		return
	}
	if len(res) < 2 {
		return
	}
	return strings.TrimSpace(res)[0] == 'y'
}
