package nodeconfig

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
	"github.com/rancher/rancher/pkg/jailer"
	"github.com/rancher/rancher/pkg/settings"
	"github.com/sirupsen/logrus"
)

type TLSConfig struct {
	Certificate, CA, Key []byte
	Address              string
}

func (t *TLSConfig) ToConfig() (*tls.Config, error) {
	cert, err := tls.X509KeyPair(t.Certificate, t.Key)
	if err != nil {
		return nil, errors.Wrap(err, "failed to load cert")
	}

	// Verify client certificates against a CA?
	certPool := x509.NewCertPool()
	certPool.AppendCertsFromPEM(t.CA)

	return &tls.Config{
		NextProtos:   []string{"http/1.1"},
		MinVersion:   tls.VersionTLS10,
		Certificates: []tls.Certificate{cert},
		RootCAs:      certPool,
	}, nil
}

func ExtractConfigJSON(extractedConfig string) (map[string]interface{}, error) {
	result := map[string]interface{}{}

	configBytes, err := base64.StdEncoding.DecodeString(extractedConfig)
	if err != nil {
		return nil, fmt.Errorf("error reinitializing config (base64.DecodeString): %v", err)
	}

	gzipReader, err := gzip.NewReader(bytes.NewReader(configBytes))
	if err != nil {
		return nil, err
	}
	tarReader := tar.NewReader(gzipReader)

	for {
		header, err := tarReader.Next()
		if err != nil {
			if err == io.EOF {
				return result, nil
			}
			return nil, fmt.Errorf("error reinitializing config (tarRead.Next): %v", err)
		}

		info := header.FileInfo()
		if info.IsDir() {
			continue
		}

		filename := header.Name
		if strings.Contains(filename, "/machines/") && strings.HasSuffix(filename, "/config.json") {
			buf := &bytes.Buffer{}
			_, err = io.Copy(buf, tarReader)
			if err != nil {
				return nil, fmt.Errorf("error reinitializing config (Copy): %v", err)
			}

			if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
				return nil, errors.Wrap(err, "failed to read config.json")
			}
		}
	}
}

func extractTLS(extractedConfig string) (*TLSConfig, error) {
	result := &TLSConfig{}

	configBytes, err := base64.StdEncoding.DecodeString(extractedConfig)
	if err != nil {
		return nil, fmt.Errorf("error reinitializing config (base64.DecodeString): %v", err)
	}

	gzipReader, err := gzip.NewReader(bytes.NewReader(configBytes))
	if err != nil {
		return nil, err
	}
	tarReader := tar.NewReader(gzipReader)

	for {
		header, err := tarReader.Next()
		if err != nil {
			if err == io.EOF {
				return result, nil
			}
			return nil, fmt.Errorf("error reinitializing config (tarRead.Next): %v", err)
		}

		info := header.FileInfo()
		if info.IsDir() {
			continue
		}

		filename := header.Name
		buf := &bytes.Buffer{}

		_, err = io.Copy(buf, tarReader)
		if err != nil {
			return nil, fmt.Errorf("error reinitializing config (Copy): %v", err)
		}

		if strings.Contains(filename, "/machines/") {
			if strings.HasSuffix(filename, "/key.pem") {
				result.Key = buf.Bytes()
			} else if strings.HasSuffix(filename, "/cert.pem") {
				result.Certificate = buf.Bytes()
			} else if strings.HasSuffix(filename, "/ca.pem") {
				result.CA = buf.Bytes()
			} else if strings.HasSuffix(filename, "/config.json") {
				data := map[string]interface{}{}
				if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
					return nil, errors.Wrap(err, "failed to read config.json")
				}
				data, _ = data["Driver"].(map[string]interface{})
				result.Address, _ = data["IPAddress"].(string)
				if result.Address != "" {
					result.Address += ":2376"
				}
			}
		}
	}
}

func extractConfig(baseDir, extractedConfig string) error {
	baseDir = filepath.Dir(baseDir)
	configBytes, err := base64.StdEncoding.DecodeString(extractedConfig)
	if err != nil {
		return fmt.Errorf("error reinitializing config (base64.DecodeString). Config Dir: %v. Error: %v", baseDir, err)
	}

	gzipReader, err := gzip.NewReader(bytes.NewReader(configBytes))
	if err != nil {
		return err
	}
	tarReader := tar.NewReader(gzipReader)

	for {
		header, err := tarReader.Next()
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return fmt.Errorf("error reinitializing config (tarRead.Next). Config Dir: %v. Error: %v", baseDir, err)
		}

		filename := header.Name
		filePath := filepath.Join(baseDir, filename)
		logrus.Debugf("Extracting %v", filePath)

		info := header.FileInfo()
		if info.IsDir() {
			err = os.MkdirAll(filePath, os.FileMode(header.Mode))
			if err != nil {
				return fmt.Errorf("error reinitializing config (Mkdirall). Config Dir: %v. Dir: %v. Error: %v", baseDir, info.Name(), err)
			}
			if os.Getenv("CATTLE_DEV_MODE") == "" && settings.UnprivilegedJailUser.Get() == "true" {
				if err := jailer.SetJailOwnership(filePath); err != nil {
					return fmt.Errorf("error updating perms for %s: %w", filePath, err)
				}
			} else {
				// Make sure to preserve the directory UID and GID.
				if err = os.Chown(filePath, header.Uid, header.Gid); err != nil {
					return fmt.Errorf("error changing ownership of directory %s: %w", filePath, err)
				}
			}
			continue
		}

		file, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
		if err != nil {
			return fmt.Errorf("error reinitializing config (OpenFile). Config Dir: %v. File: %v. Error: %v", baseDir, info.Name(), err)
		}
		if os.Getenv("CATTLE_DEV_MODE") == "" && settings.UnprivilegedJailUser.Get() == "true" {
			if err := jailer.SetJailOwnership(filePath); err != nil {
				return fmt.Errorf("error updating perms for %s: %w", filePath, err)
			}
		} else {
			// Make sure to preserve the file UID and GID.
			if err = os.Chown(filePath, header.Uid, header.Gid); err != nil {
				return fmt.Errorf("error changing ownership of file %s: %w", filePath, err)
			}
		}
		_, err = io.Copy(file, tarReader)
		file.Close()
		if err != nil {
			return fmt.Errorf("error reinitializing config (Copy). Config Dir: %v. File: %v. Error: %v", baseDir, info.Name(), err)
		}
	}
}

func compressConfig(baseDir string) (string, error) {
	// create the tar.gz file
	destFile := &bytes.Buffer{}

	fileWriter := gzip.NewWriter(destFile)
	tarfileWriter := tar.NewWriter(fileWriter)

	if err := addDirToArchive(baseDir, tarfileWriter); err != nil {
		return "", err
	}

	tarfileWriter.Close()
	fileWriter.Close()

	return base64.StdEncoding.EncodeToString(destFile.Bytes()), nil
}

func addDirToArchive(source string, tarfileWriter *tar.Writer) error {
	baseDir := filepath.Base(source)

	return filepath.Walk(source,
		func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}

			if path == source || strings.HasSuffix(info.Name(), ".iso") ||
				strings.HasSuffix(info.Name(), ".tar.gz") ||
				strings.HasSuffix(info.Name(), ".vmdk") ||
				strings.HasSuffix(info.Name(), ".img") {
				return nil
			}

			header, err := tar.FileInfoHeader(info, info.Name())
			if err != nil {
				return err
			}

			header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))

			if err := tarfileWriter.WriteHeader(header); err != nil {
				return err
			}

			if info.IsDir() {
				return nil
			}

			file, err := os.Open(path)
			if err != nil {
				return err
			}
			defer file.Close()

			_, err = io.Copy(tarfileWriter, file)
			return err
		})
}
