package object

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"golang.org/x/term"

	"github.com/opensvc/om3/v3/core/keyop"
	"github.com/opensvc/om3/v3/core/oc3path"
	"github.com/opensvc/om3/v3/util/hostname"
	"github.com/opensvc/om3/v3/util/key"
)

type (
	// registerReq structures the POST /register request body
	registerReq struct {
		Nodename string `json:"nodename"`
		App      string `json:"app,omitempty"`
	}

	// registerRes structures the POST /register response body
	registerRes struct {
		Data  registerResData `json:"data"`
		Info  string          `json:"info"`
		Error string          `json:"error"`
	}
	registerResData struct {
		UUID string `json:"uuid"`
	}

	// registerResOK structures the successful POST oc3 node register response body
	registerResOK struct {
		UUID string `json:"uuid"`
		Info string `json:"info"`
	}
)

// Register logs in to the collector using the provided user credentials.
//
// If the node is not already known to the collector, a new node uuid is
// generated by the collector and stored by the agent in the node config
// as a node.uuid key.
//
// If user credentials are given, the POST /register collector handler is
// used, and the app code is supported.
// If no user credentials are given, the "register_node" jsonrpc handler
// is used, and app code is ignored.
//
// If app is not set, the node is added to any app under the user's
// responsibility.
func (t Node) Register(ctx context.Context, user, password, app string) error {
	if err := t.register(user, password, app); err != nil {
		return fmt.Errorf("register: %w", err)
	}
	if _, err := t.PushAsset(); err != nil {
		return fmt.Errorf("push asset: %w", err)
	} else {
		t.Log().Infof("sent initial asset discovery")
	}
	if data, err := t.PushPkg(); err != nil {
		return fmt.Errorf("push pkg: %w", err)
	} else {
		t.Log().Infof("sent initial package inventory (%d)", len(data))
	}
	if data, err := t.PushPatch(); err != nil {
		// TODO: implement oc3 post patch
		//return fmt.Errorf("push patch: %w", err)
		t.Log().Warnf("push patch: %s", err)
	} else {
		t.Log().Infof("sent initial patch inventory (%d)", len(data))
	}
	if _, err := t.PushDisks(); err != nil {
		return fmt.Errorf("push disk: %w", err)
	}
	t.Log().Infof("sent initial disk inventory")

	if err := t.Sysreport(); err != nil {
		// TODO: implement oc3 sysreport
		// return fmt.Errorf("sysreport: %w", err)
		t.Log().Warnf("sysreport: %s", err)
	} else {
		t.Log().Infof("sent initial sysreport")
	}

	if data, err := t.Checks(ctx); err != nil {
		// TODO: implement oc3 push check
		// return fmt.Errorf("push check: %w", err)
		t.Log().Warnf("push check: %s", err)
	} else {
		t.Log().Infof("sent initial checks (%d)", data.Len())
	}
	return nil
}

func (t Node) register(user, password, app string) error {
	if user == "" {
		return t.registerAsNode()
	} else {
		if t.CollectorRawConfig().HasServerV3() {
			return t.registerAsUserV3(user, password, app)
		} else {
			return t.registerAsUserV2(user, password, app)
		}
	}
}

func (t Node) registerAsUserV3(user, password, app string) error {
	var (
		req  *http.Request
		resp *http.Response

		ioReader io.Reader

		method = http.MethodPost
		path   = oc3path.ServerRegister
	)
	if app == "" {
		app = t.MergedConfig().GetString(key.Parse("node.app"))
	}
	if password == "" {
		fmt.Printf("Password: ")
		if b, err := term.ReadPassword(int(os.Stdin.Fd())); err != nil {
			return err
		} else {
			password = string(b)
			fmt.Println("")
		}
	}
	auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
	oc3, err := t.CollectorServerWithAuth(auth)
	if err != nil {
		return fmt.Errorf("create collector server: %w", err)
	}
	reqData := registerReq{
		Nodename: hostname.Hostname(),
		App:      app,
	}
	if b, err := json.Marshal(reqData); err != nil {
		return fmt.Errorf("encode request body:%w", err)
	} else {
		ioReader = bytes.NewBuffer(b)
	}

	ctx, cancel := context.WithTimeout(context.Background(), defaultPostCollectorTimeout)
	defer cancel()

	req, err = oc3.NewRequestWithContext(ctx, method, path, ioReader)
	if err != nil {
		return fmt.Errorf("create collector request %s %s: %w", method, path, err)
	}

	resp, err = oc3.Do(req)
	if err != nil {
		return fmt.Errorf("collector %s %s: %w", method, path, err)
	}
	defer func() { _ = resp.Body.Close() }()

	if err := CollectorResponseStatusCheck(resp, method, path, []int{http.StatusOK}); err != nil {
		return err
	}
	dec := json.NewDecoder(resp.Body)
	data := registerResOK{}
	if err := dec.Decode(&data); err != nil {
		return fmt.Errorf("decode response body: %w", err)
	}
	if data.Info != "" {
		t.Log().Infof("%s", data.Info)
	}
	return t.writeUUID(data.UUID)
}

func (t Node) registerAsUserV2(user, password, app string) error {
	if password == "" {
		fmt.Printf("Password: ")
		if b, err := term.ReadPassword(int(os.Stdin.Fd())); err != nil {
			return err
		} else {
			password = string(b)
			fmt.Println("")
		}
	}
	client := t.CollectorRestAPIClient()
	url, err := t.CollectorRestAPIURL()
	if err != nil {
		return err
	}
	url.Path += "/register"
	if app == "" {
		app = t.MergedConfig().GetString(key.Parse("node.app"))
	}
	reqData := registerReq{
		Nodename: hostname.Hostname(),
		App:      app,
	}
	b, err := json.Marshal(reqData)
	if err != nil {
		return fmt.Errorf("encode request body: %w", err)
	}
	req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewBuffer(b))
	req.SetBasicAuth(user, password)
	req.Header.Add("Content-Type", "application/json")
	response, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("do request: %w", err)
	}
	defer response.Body.Close()
	if response.StatusCode != 200 {
		if b, err := io.ReadAll(response.Body); err != nil {
			return fmt.Errorf("%d: %s", response.StatusCode, response.Status)
		} else {
			return fmt.Errorf("%s", string(b))
		}
	}
	dec := json.NewDecoder(response.Body)
	data := registerRes{}
	if err := dec.Decode(&data); err != nil {
		return fmt.Errorf("decode response body: %w", err)
	}
	if data.Error != "" {
		return errors.New(data.Error)
	}
	if data.Info != "" {
		t.Log().Infof("%s", data.Info)
	}
	return t.writeUUID(data.Data.UUID)
}

func (t Node) registerAsNode() error {
	client, err := t.CollectorFeedClient()
	if err != nil {
		return err
	}
	if response, err := client.Call("register_node"); err != nil {
		return err
	} else if response.Error != nil {
		return fmt.Errorf("rpc: %s: %s", response.Error.Message, response.Error.Data)
	} else if response.Result != nil {
		switch v := response.Result.(type) {
		case []interface{}:
			for _, e := range v {
				s, ok := e.(string)
				if !ok {
					continue
				}
				if strings.Contains(s, "already") {
					t.Log().Infof(s)
				} else {
					return errors.New(s)
				}
			}
		case string:
			return t.writeUUID(v)
		default:
			return fmt.Errorf("unknown response result type: %+v", v)
		}
	} else {
		return fmt.Errorf("unexpected rpc response: %+v", response)
	}
	return nil
}

func (t Node) writeUUID(s string) error {
	if current := t.Config().GetString(key.Parse("node.uuid")); current == s {
		return nil
	}
	op := keyop.Parse("node.uuid=" + s)
	return t.Config().Set(*op)
}
