//go:build linux

package executor

import (
	"bytes"
	"crypto/sha256"
	"crypto/x509"
	"encoding/pem"
	"io"
	"os"
	"path/filepath"
	"syscall"

	"github.com/containerd/continuity/fs"
	"github.com/pkg/errors"
)

var linuxSystemCertFiles = []string{
	"/etc/ssl/certs/ca-certificates.crt",
	"/etc/pki/tls/certs/ca-bundle.crt",
	"/etc/ssl/ca-bundle.pem",
	"/etc/pki/tls/cacert.pem",
	"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
	"/etc/ssl/cert.pem",
}

var (
	proxyCABegin = []byte("\n# buildkit proxy CA begin\n")
	proxyCAEnd   = []byte("# buildkit proxy CA end\n")
)

const maxCertBundleBytes = 10 << 20

// InjectProxyCA appends caPEM to the rootfs trust bundle used by common Linux
// TLS stacks and returns a cleanup that removes only the injected CA.
func InjectProxyCA(rootfsPath string, caPEM []byte) (func() error, error) {
	if len(caPEM) == 0 {
		return func() error { return nil }, nil
	}
	cert, err := firstCertificate(caPEM)
	if err != nil {
		return nil, err
	}
	certSum := sha256.Sum256(cert.Raw)

	var bundle string
	for _, name := range linuxSystemCertFiles {
		p, err := fs.RootPath(rootfsPath, name)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to resolve certificate bundle %s", name)
		}
		if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() {
			bundle = p
			break
		}
	}
	if bundle == "" {
		return func() error { return nil }, nil
	}

	original, st, err := readCertBundle(bundle)
	if err != nil {
		return nil, err
	}
	if containsCertificate(original, certSum) {
		return func() error { return nil }, nil
	}
	next := append([]byte{}, original...)
	if len(next) > 0 && next[len(next)-1] != '\n' {
		next = append(next, '\n')
	}
	next = append(next, proxyCABegin...)
	next = append(next, caPEM...)
	if len(next) > 0 && next[len(next)-1] != '\n' {
		next = append(next, '\n')
	}
	next = append(next, proxyCAEnd...)
	if err := writeCertBundle(bundle, next, st); err != nil {
		return nil, err
	}

	return func() error {
		current, st, err := readCertBundle(bundle)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				return nil
			}
			return err
		}
		cleaned := removeInjectedCA(current, certSum)
		if bytes.Equal(current, cleaned) {
			return nil
		}
		return writeCertBundle(bundle, cleaned, st)
	}, nil
}

func readCertBundle(path string) ([]byte, os.FileInfo, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, nil, errors.WithStack(err)
	}
	defer f.Close()

	st, err := f.Stat()
	if err != nil {
		return nil, nil, errors.WithStack(err)
	}
	if !st.Mode().IsRegular() {
		return nil, nil, errors.Errorf("%s is not a regular file", path)
	}

	limited := &io.LimitedReader{R: f, N: maxCertBundleBytes + 1}
	dt, err := io.ReadAll(limited)
	if err != nil {
		return nil, nil, errors.WithStack(err)
	}
	if limited.N == 0 {
		return nil, nil, errors.Errorf("%s exceeds %d bytes", path, maxCertBundleBytes)
	}
	return dt, st, nil
}

func firstCertificate(dt []byte) (*x509.Certificate, error) {
	for {
		block, rest := pem.Decode(dt)
		if block == nil {
			return nil, errors.New("proxy CA PEM does not contain a certificate")
		}
		dt = rest
		if block.Type != "CERTIFICATE" {
			continue
		}
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		return cert, nil
	}
}

func containsCertificate(dt []byte, sum [sha256.Size]byte) bool {
	for {
		block, rest := pem.Decode(dt)
		if block == nil {
			return false
		}
		dt = rest
		if block.Type != "CERTIFICATE" {
			continue
		}
		if sha256.Sum256(block.Bytes) == sum {
			return true
		}
	}
}

func removeInjectedCA(dt []byte, sum [sha256.Size]byte) []byte {
	begin := bytes.Index(dt, proxyCABegin)
	if begin >= 0 {
		end := bytes.Index(dt[begin+len(proxyCABegin):], proxyCAEnd)
		if end >= 0 {
			end += begin + len(proxyCABegin) + len(proxyCAEnd)
			block := dt[begin:end]
			if containsCertificate(block, sum) {
				out := append([]byte{}, dt[:begin]...)
				out = append(out, dt[end:]...)
				return out
			}
		}
	}

	var out []byte
	for len(dt) > 0 {
		idx := bytes.Index(dt, []byte("-----BEGIN "))
		if idx < 0 {
			out = append(out, dt...)
			break
		}
		out = append(out, dt[:idx]...)
		block, rest := pem.Decode(dt[idx:])
		if block == nil {
			out = append(out, dt[idx:]...)
			break
		}
		consumed := len(dt[idx:]) - len(rest)
		if block.Type != "CERTIFICATE" || sha256.Sum256(block.Bytes) != sum {
			out = append(out, dt[idx:idx+consumed]...)
		}
		dt = rest
	}
	return out
}

func writeCertBundle(path string, dt []byte, st os.FileInfo) error {
	tmp, err := os.CreateTemp(filepath.Dir(path), ".buildkit-ca-*")
	if err != nil {
		return errors.WithStack(err)
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	if _, err := tmp.Write(dt); err != nil {
		tmp.Close()
		return errors.WithStack(err)
	}
	if err := tmp.Chmod(st.Mode()); err != nil {
		tmp.Close()
		return errors.WithStack(err)
	}
	if sys, ok := st.Sys().(*syscall.Stat_t); ok {
		if err := tmp.Chown(int(sys.Uid), int(sys.Gid)); err != nil {
			tmp.Close()
			return errors.WithStack(err)
		}
	}
	if err := tmp.Close(); err != nil {
		return errors.WithStack(err)
	}
	return errors.WithStack(os.Rename(tmpName, path))
}
