package build

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"text/template"
	"time"
)

type Config struct {
	Meta struct {
		BuildDatetime          string // generated field
		PackageMaintainer      string `json:"package_maintainer"`
		PackageMaintainerEmail string `json:"package_maintainer_email"`
	}
	Env struct {
		Version              string `json:"version"`
		BackendInstallPath   string `json:"backend_install_path"`
		BackendDataPath      string `json:"backend_data_path"`
		BlockbookInstallPath string `json:"blockbook_install_path"`
		BlockbookDataPath    string `json:"blockbook_data_path"`
	} `json:"env"`
	Coin struct {
		Name     string `json:"name"`
		Shortcut string `json:"shortcut"`
		Label    string `json:"label"`
		Alias    string `json:"alias"`
	} `json:"coin"`
	Ports struct {
		BackendRPC          int `json:"backend_rpc"`
		BackendMessageQueue int `json:"backend_message_queue"`
		BlockbookInternal   int `json:"blockbook_internal"`
		BlockbookPublic     int `json:"blockbook_public"`
	} `json:"ports"`
	IPC struct {
		RPCURLTemplate              string `json:"rpc_url_template"`
		RPCUser                     string `json:"rpc_user"`
		RPCPass                     string `json:"rpc_pass"`
		RPCTimeout                  int    `json:"rpc_timeout"`
		MessageQueueBindingTemplate string `json:"message_queue_binding_template"`
	} `json:"ipc"`
	Backend struct {
		PackageName                     string      `json:"package_name"`
		PackageRevision                 string      `json:"package_revision"`
		SystemUser                      string      `json:"system_user"`
		Version                         string      `json:"version"`
		BinaryURL                       string      `json:"binary_url"`
		VerificationType                string      `json:"verification_type"`
		VerificationSource              string      `json:"verification_source"`
		ExtractCommand                  string      `json:"extract_command"`
		ExcludeFiles                    []string    `json:"exclude_files"`
		ExecCommandTemplate             string      `json:"exec_command_template"`
		LogrotateFilesTemplate          string      `json:"logrotate_files_template"`
		PostinstScriptTemplate          string      `json:"postinst_script_template"`
		ServiceType                     string      `json:"service_type"`
		ServiceAdditionalParamsTemplate string      `json:"service_additional_params_template"`
		ProtectMemory                   bool        `json:"protect_memory"`
		Mainnet                         bool        `json:"mainnet"`
		ServerConfigFile                string      `json:"server_config_file"`
		ClientConfigFile                string      `json:"client_config_file"`
		AdditionalParams                interface{} `json:"additional_params"`
	} `json:"backend"`
	Blockbook struct {
		PackageName             string `json:"package_name"`
		SystemUser              string `json:"system_user"`
		InternalBindingTemplate string `json:"internal_binding_template"`
		PublicBindingTemplate   string `json:"public_binding_template"`
		ExplorerURL             string `json:"explorer_url"`
		AdditionalParams        string `json:"additional_params"`
		BlockChain              struct {
			Parse                bool                       `json:"parse"`
			Subversion           string                     `json:"subversion"`
			AddressFormat        string                     `json:"address_format"`
			MempoolWorkers       int                        `json:"mempool_workers"`
			MempoolSubWorkers    int                        `json:"mempool_sub_workers"`
			BlockAddressesToKeep int                        `json:"block_addresses_to_keep"`
			AdditionalParams     map[string]json.RawMessage `json:"additional_params"`
		} `json:"block_chain"`
	} `json:"blockbook"`
	IntegrationTests map[string][]string `json:"integration_tests"`
}

func jsonToString(msg json.RawMessage) (string, error) {
	d, err := msg.MarshalJSON()
	if err != nil {
		return "", err
	}
	return string(d), nil
}

func generateRPCAuth(user, pass string) (string, error) {
	cmd := exec.Command("/bin/bash", "-c", "build/scripts/rpcauth.py \"$0\" \"$1\" | sed -n -e 2p", user, pass)
	var out bytes.Buffer
	cmd.Stdout = &out
	err := cmd.Run()
	if err != nil {
		return "", err
	}
	return out.String(), nil
}

func (c *Config) ParseTemplate() *template.Template {
	templates := map[string]string{
		"IPC.RPCURLTemplate":                      c.IPC.RPCURLTemplate,
		"IPC.MessageQueueBindingTemplate":         c.IPC.MessageQueueBindingTemplate,
		"Backend.ExecCommandTemplate":             c.Backend.ExecCommandTemplate,
		"Backend.LogrotateFilesTemplate":          c.Backend.LogrotateFilesTemplate,
		"Backend.PostinstScriptTemplate":          c.Backend.PostinstScriptTemplate,
		"Backend.ServiceAdditionalParamsTemplate": c.Backend.ServiceAdditionalParamsTemplate,
		"Blockbook.InternalBindingTemplate":       c.Blockbook.InternalBindingTemplate,
		"Blockbook.PublicBindingTemplate":         c.Blockbook.PublicBindingTemplate,
	}

	funcMap := template.FuncMap{
		"jsonToString":    jsonToString,
		"generateRPCAuth": generateRPCAuth,
	}

	t := template.New("").Funcs(funcMap)

	for name, def := range templates {
		t = template.Must(t.Parse(fmt.Sprintf(`{{define "%s"}}%s{{end}}`, name, def)))
	}

	return t
}

func LoadConfig(configsDir, coin string) (*Config, error) {
	config := new(Config)

	f, err := os.Open(filepath.Join(configsDir, "coins", coin+".json"))
	if err != nil {
		return nil, err
	}
	d := json.NewDecoder(f)
	err = d.Decode(config)
	if err != nil {
		return nil, err
	}

	f, err = os.Open(filepath.Join(configsDir, "environ.json"))
	if err != nil {
		return nil, err
	}
	d = json.NewDecoder(f)
	err = d.Decode(&config.Env)
	if err != nil {
		return nil, err
	}

	config.Meta.BuildDatetime = time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700")

	if !isEmpty(config, "backend") {
		switch config.Backend.ServiceType {
		case "forking":
		case "simple":
		default:
			return nil, fmt.Errorf("Invalid service type: %s", config.Backend.ServiceType)
		}

		switch config.Backend.VerificationType {
		case "":
		case "gpg":
		case "sha256":
		case "gpg-sha256":
		default:
			return nil, fmt.Errorf("Invalid verification type: %s", config.Backend.VerificationType)
		}
	}

	return config, nil
}

func isEmpty(config *Config, target string) bool {
	switch target {
	case "backend":
		return config.Backend.PackageName == ""
	case "blockbook":
		return config.Blockbook.PackageName == ""
	default:
		panic("Invalid target name: " + target)
	}
}

func GeneratePackageDefinitions(config *Config, templateDir, outputDir string) error {
	templ := config.ParseTemplate()

	err := makeOutputDir(outputDir)
	if err != nil {
		return err
	}

	for _, subdir := range []string{"backend", "blockbook"} {
		if isEmpty(config, subdir) {
			continue
		}

		root := filepath.Join(templateDir, subdir)

		err = os.Mkdir(filepath.Join(outputDir, subdir), 0755)
		if err != nil {
			return err
		}

		err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return fmt.Errorf("%s: %s", path, err)
			}

			if path == root {
				return nil
			}
			if filepath.Base(path)[0] == '.' {
				return nil
			}

			subpath := path[len(root)-len(subdir):]

			if info.IsDir() {
				err = os.Mkdir(filepath.Join(outputDir, subpath), info.Mode())
				if err != nil {
					return fmt.Errorf("%s: %s", path, err)
				}
				return nil
			}

			t := template.Must(templ.Clone())
			t = template.Must(t.ParseFiles(path))

			err = writeTemplate(filepath.Join(outputDir, subpath), info, t, config)
			if err != nil {
				return fmt.Errorf("%s: %s", path, err)
			}

			return nil
		})
		if err != nil {
			return err
		}
	}

	if !isEmpty(config, "backend") {
		err = writeBackendServerConfigFile(config, outputDir)
		if err == nil {
			err = writeBackendClientConfigFile(config, outputDir)
		}
		if err != nil {
			return err
		}
	}

	return nil
}

func makeOutputDir(path string) error {
	err := os.RemoveAll(path)
	if err == nil {
		err = os.Mkdir(path, 0755)
	}
	return err
}

func writeTemplate(path string, info os.FileInfo, templ *template.Template, config *Config) error {
	f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, info.Mode())
	if err != nil {
		return err
	}
	defer f.Close()

	err = templ.ExecuteTemplate(f, "main", config)
	if err != nil {
		return err
	}

	return nil
}

func writeBackendServerConfigFile(config *Config, outputDir string) error {
	out, err := os.OpenFile(filepath.Join(outputDir, "backend/server.conf"), os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer out.Close()

	if config.Backend.ServerConfigFile == "" {
		return nil
	} else {
		in, err := os.Open(filepath.Join(outputDir, "backend/config", config.Backend.ServerConfigFile))
		if err != nil {
			return err
		}
		defer in.Close()

		_, err = io.Copy(out, in)
		if err != nil {
			return err
		}
	}

	return nil
}

func writeBackendClientConfigFile(config *Config, outputDir string) error {
	out, err := os.OpenFile(filepath.Join(outputDir, "backend/client.conf"), os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer out.Close()

	if config.Backend.ClientConfigFile == "" {
		return nil
	} else {
		in, err := os.Open(filepath.Join(outputDir, "backend/config", config.Backend.ClientConfigFile))
		if err != nil {
			return err
		}
		defer in.Close()

		_, err = io.Copy(out, in)
		if err != nil {
			return err
		}
	}

	return nil
}
