package bake

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"slices"
	"strings"

	"github.com/containerd/console"
	"github.com/docker/buildx/build"
	"github.com/moby/buildkit/util/entitlements"
	"github.com/pkg/errors"
)

type EntitlementKey string

const (
	EntitlementKeyNetworkHost      EntitlementKey = "network.host"
	EntitlementKeySecurityInsecure EntitlementKey = "security.insecure"
	EntitlementKeyFSRead           EntitlementKey = "fs.read"
	EntitlementKeyFSWrite          EntitlementKey = "fs.write"
	EntitlementKeyFS               EntitlementKey = "fs"
	EntitlementKeyImagePush        EntitlementKey = "image.push"
	EntitlementKeyImageLoad        EntitlementKey = "image.load"
	EntitlementKeyImage            EntitlementKey = "image"
	EntitlementKeySSH              EntitlementKey = "ssh"
)

type EntitlementConf struct {
	NetworkHost      bool
	SecurityInsecure bool
	FSRead           []string
	FSWrite          []string
	ImagePush        []string
	ImageLoad        []string
	SSH              bool
}

func ParseEntitlements(in []string) (EntitlementConf, error) {
	var conf EntitlementConf
	for _, e := range in {
		switch e {
		case string(EntitlementKeyNetworkHost):
			conf.NetworkHost = true
		case string(EntitlementKeySecurityInsecure):
			conf.SecurityInsecure = true
		case string(EntitlementKeySSH):
			conf.SSH = true
		default:
			k, v, _ := strings.Cut(e, "=")
			switch k {
			case string(EntitlementKeyFSRead):
				conf.FSRead = append(conf.FSRead, v)
			case string(EntitlementKeyFSWrite):
				conf.FSWrite = append(conf.FSWrite, v)
			case string(EntitlementKeyFS):
				conf.FSRead = append(conf.FSRead, v)
				conf.FSWrite = append(conf.FSWrite, v)
			case string(EntitlementKeyImagePush):
				conf.ImagePush = append(conf.ImagePush, v)
			case string(EntitlementKeyImageLoad):
				conf.ImageLoad = append(conf.ImageLoad, v)
			case string(EntitlementKeyImage):
				conf.ImagePush = append(conf.ImagePush, v)
				conf.ImageLoad = append(conf.ImageLoad, v)
			default:
				return conf, errors.Errorf("uknown entitlement key %q", k)
			}

			// TODO: dedupe slices and parent paths
		}
	}
	return conf, nil
}

func (c EntitlementConf) Validate(m map[string]build.Options) (EntitlementConf, error) {
	var expected EntitlementConf

	for _, v := range m {
		if err := c.check(v, &expected); err != nil {
			return EntitlementConf{}, err
		}
	}

	return expected, nil
}

func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) error {
	for _, e := range bo.Allow {
		switch e {
		case entitlements.EntitlementNetworkHost:
			if !c.NetworkHost {
				expected.NetworkHost = true
			}
		case entitlements.EntitlementSecurityInsecure:
			if !c.SecurityInsecure {
				expected.SecurityInsecure = true
			}
		}
	}
	return nil
}

func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
	var term bool
	if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
		term = true
	}

	var msgs []string
	var flags []string

	if c.NetworkHost {
		msgs = append(msgs, " - Running build containers that can access host network")
		flags = append(flags, "network.host")
	}
	if c.SecurityInsecure {
		msgs = append(msgs, " - Running privileged containers that can make system changes")
		flags = append(flags, "security.insecure")
	}

	if len(msgs) == 0 {
		return nil
	}

	fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
	for _, m := range msgs {
		fmt.Fprintf(out, "%s\n", m)
	}

	for i, f := range flags {
		flags[i] = "--allow=" + f
	}

	if term {
		fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
	} else {
		fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
	}

	args := append([]string(nil), os.Args...)
	if v, ok := os.LookupEnv("DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"); ok && v != "" {
		args[0] = v
	}
	idx := slices.Index(args, "bake")

	if idx != -1 {
		fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
		fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
	}

	if term {
		fmt.Fprintf(out, "Do you want to grant requested privileges and continue? [y/N] ")
		reader := bufio.NewReader(os.Stdin)
		answerCh := make(chan string, 1)
		go func() {
			answer, _, _ := reader.ReadLine()
			answerCh <- string(answer)
			close(answerCh)
		}()

		select {
		case <-ctx.Done():
		case answer := <-answerCh:
			if strings.ToLower(string(answer)) == "y" {
				return nil
			}
		}
	}

	return errors.Errorf("additional privileges requested")
}
