package es

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"

	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/config"
)

type BaseError struct {
	Type   string `json:"type"`
	Reason string `json:"reason"`
	Index  string `json:"index"`
}

func (e BaseError) Error() string {
	return fmt.Sprintf("type: %s, reason: %s, index: %s", e.Type, e.Reason, e.Index)
}

type BaseResult struct {
	Error  *BaseError `json:"error"`
	Status int        `json:"status"`
}

const (
	indexNotFoundException = "index_not_found_exception"
)

// QueryDataNumByIndex return <count, index found, error>
func QueryDataNumByIndex(index string, lc config.LogConfig) (uint64, bool, error) { // todo@toto change clusterid to index_name
	path := fmt.Sprintf("/%s/_count", index)
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s%s", lc.Endpoint, path), nil)
	if err != nil {
		log.Error("new request failed", zap.Error(err))
		return 0, false, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("osd-xsrf", "true")
	req.SetBasicAuth(lc.User, lc.Password)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Error("request for delete log data failed", zap.Error(err))
		return 0, false, err
	}
	defer func() {
		//_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()

	if resp.StatusCode != 200 {
		ret := new(BaseResult)
		_ = json.NewDecoder(resp.Body).Decode(ret)
		// if the index is not existed, we should ignore the error and return 0
		if ret.Status == http.StatusNotFound && ret.Error.Type == indexNotFoundException {
			return 0, false, nil
		}
		log.Error("query log count failed", zap.Int("code", resp.StatusCode), zap.String("es_reason", ret.Error.Reason), zap.String("es_type", ret.Error.Type))
		return 0, false, fmt.Errorf("query count error: %w", ret.Error)
	}
	data, _ := io.ReadAll(resp.Body)
	respCount := struct {
		Count uint64 `json:"count"`
	}{}
	if err := json.Unmarshal(data, &respCount); err != nil {
		log.Error("unmarshal data failed", zap.Any("response", data))
		return 0, false, err
	}
	return respCount.Count, true, nil
}

func DeleteIndex(index string, lc config.LogConfig) error {
	path := fmt.Sprintf("/%s", index)
	req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s%s", lc.Endpoint, path), nil)
	if err != nil {
		log.Error("new request failed", zap.Error(err))
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("osd-xsrf", "true")
	req.SetBasicAuth(lc.User, lc.Password)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Error("request for delete log data failed", zap.Error(err))
		return err
	}
	defer func() {
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()

	if resp.StatusCode != 200 {
		ret := new(BaseResult)
		_ = json.NewDecoder(resp.Body).Decode(ret)
		// if the index is not existed, we should ignore the error
		if ret.Status == http.StatusNotFound && ret.Error.Type == indexNotFoundException {
			return nil
		}
		log.Error("delete log data failed", zap.Int("code", resp.StatusCode), zap.String("es_reason", ret.Error.Reason), zap.String("es_type", ret.Error.Type))
		return fmt.Errorf("delete log error: %w", ret.Error)
	}
	return nil
}

func DeleteByCluster(index, clusterID string, lc config.LogConfig) error {
	data := `
	{
		"query": {
		  "match": {
			"cluster_id": "%s"
		  }
		}
	}`
	path := fmt.Sprintf("/%s/_delete_by_query", index)
	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", lc.Endpoint, path), bytes.NewBufferString(fmt.Sprintf(data, clusterID)))
	if err != nil {
		log.Error("new request failed", zap.Error(err))
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("osd-xsrf", "true")
	req.SetBasicAuth(lc.User, lc.Password)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Error("request for delete log data failed", zap.Error(err))
		return err
	}
	defer func() {
		//_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()
	if resp.StatusCode != 200 {
		data, _ := io.ReadAll(resp.Body)
		log.Error("delete log data failed", zap.Int("code", resp.StatusCode), zap.String("status", resp.Status), zap.String("response", string(data)))
		return errors.New("delete log and return != 200")
	}
	return nil
}

func DeleteByFileUUID(index, fileUUID string, lc config.LogConfig) error {
	//nolint:misspell
	data := `
	{
		"query": {
		  "match": {
			"packageid": "%s"
		  }
		}
	  }`
	path := fmt.Sprintf("/%s/_delete_by_query?conflicts=proceed&wait_for_completion=false", index)
	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", lc.Endpoint, path), bytes.NewBufferString(fmt.Sprintf(data, fileUUID)))
	if err != nil {
		log.Error("new request failed", zap.Error(err))
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("osd-xsrf", "true")
	req.SetBasicAuth(lc.User, lc.Password)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Error("request for delete log data failed", zap.Error(err))
		return err
	}
	defer func() {
		//_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close()
	}()
	if resp.StatusCode != 200 {
		data, _ := io.ReadAll(resp.Body)
		log.Error("delete log data failed", zap.Int("code", resp.StatusCode), zap.String("status", resp.Status), zap.String("response", string(data)))
		return errors.New("delete log and return != 200")
	}
	return nil
}
