package model

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"path"
	"strings"

	"github.com/pingcap/log"
	"github.com/tidbcloud/debug_portal/config"
	"github.com/tidbcloud/debug_portal/db"
	"github.com/tidbcloud/debug_portal/db/entity"
	"github.com/tidbcloud/debug_portal/model/admin/grafana"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// DefaultHTTPClient initialized Grafana with appropriate conditions.
// It allows you globally redefine HTTP client.
var DefaultHTTPClient = http.DefaultClient

type DashboardLink struct {
	Name string
	Link string
}

// Client uses Grafana REST API for interacting with Grafana server.
type GrafanaClient struct {
	baseURL   string
	key       string
	basicAuth bool
	client    *http.Client
}

// Search searches folders and dashboards with query params specified.
//
// Reflects GET /api/search API call.
func (r *GrafanaClient) Search(ctx context.Context, params ...grafana.SearchParam) ([]grafana.FoundBoard, error) {
	var (
		raw    []byte
		boards []grafana.FoundBoard
		code   int
		err    error
	)
	u := url.URL{}
	q := u.Query()
	for _, p := range params {
		p(&q)
	}
	if raw, code, err = r.get(ctx, "api/search", q); err != nil {
		return nil, err
	}
	if code != 200 {
		return nil, fmt.Errorf("HTTP error %d: returns %s", code, raw)
	}
	err = json.Unmarshal(raw, &boards)
	return boards, err
}

// StatusMessage reflects status message as it returned by Grafana REST API.
type StatusMessage struct {
	ID      *uint   `json:"id"`
	OrgID   *uint   `json:"orgId"`
	Message *string `json:"message"`
	Slug    *string `json:"slug"`
	Version *int    `json:"version"`
	Status  *string `json:"status"`
	UID     *string `json:"uid"`
	URL     *string `json:"url"`
}

// NewClient initializes client for interacting with an instance of Grafana server;
// apiKeyOrBasicAuth accepts either 'username:password' basic authentication credentials,
// or a Grafana API key
func NewGrafranaClient(apiURL, apiKeyOrBasicAuth string, client *http.Client) *GrafanaClient {
	key := ""
	basicAuth := strings.Contains(apiKeyOrBasicAuth, ":")
	baseURL, _ := url.Parse(apiURL)
	if !basicAuth {
		key = fmt.Sprintf("Bearer %s", apiKeyOrBasicAuth)
	} else {
		parts := strings.Split(apiKeyOrBasicAuth, ":")
		baseURL.User = url.UserPassword(parts[0], parts[1])
	}
	return &GrafanaClient{baseURL: baseURL.String(), basicAuth: basicAuth, key: key, client: client}
}

func (r *GrafanaClient) get(ctx context.Context, query string, params url.Values) ([]byte, int, error) {
	return r.doRequest(ctx, "GET", query, params, nil)
}

func (r *GrafanaClient) patch(ctx context.Context, query string, params url.Values, body []byte) ([]byte, int, error) {
	return r.doRequest(ctx, "PATCH", query, params, bytes.NewBuffer(body))
}

func (r *GrafanaClient) put(ctx context.Context, query string, params url.Values, body []byte) ([]byte, int, error) {
	return r.doRequest(ctx, "PUT", query, params, bytes.NewBuffer(body))
}

func (r *GrafanaClient) post(ctx context.Context, query string, params url.Values, body []byte) ([]byte, int, error) {
	return r.doRequest(ctx, "POST", query, params, bytes.NewBuffer(body))
}

func (r *GrafanaClient) delete(ctx context.Context, query string) ([]byte, int, error) {
	return r.doRequest(ctx, "DELETE", query, nil, nil)
}

func (r *GrafanaClient) doRequest(ctx context.Context, method, query string, params url.Values, buf io.Reader) ([]byte, int, error) {
	u, _ := url.Parse(r.baseURL)
	u.Path = path.Join(u.Path, query)
	if params != nil {
		u.RawQuery = params.Encode()
	}
	req, err := http.NewRequest(method, u.String(), buf)
	if err != nil {
		return nil, 0, err
	}
	req = req.WithContext(ctx)
	if !r.basicAuth {
		req.Header.Set("Authorization", r.key)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "autograf")
	resp, err := r.client.Do(req)
	if err != nil {
		return nil, 0, err
	}
	data, err := ioutil.ReadAll(resp.Body)
	defer deferClose(resp.Body)
	return data, resp.StatusCode, err
}

func deferClose(c io.Closer) {
	if err := c.Close(); err != nil {
		log.Error("failed to close", zap.Error(err))
	}
}

func (m *Model) GetDashboardLinks(tenantID, clusterID uint64) ([]DashboardLink, error) {
	// FIXME: Currently this is completely copied from admin model.
	var (
		t       entity.Tenant
		cluster entity.Cluster
	)
	err := db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewTenantQuerySet(tx).IDEq(entity.ID(tenantID)).One(&t)
		if err != nil {
			return err
		}
		return entity.NewClusterQuerySet(tx).
			StatusNotIn(entity.ClusterStatusDeleted, entity.ClusterStatusCleared). // do we really need filter by this?
			IDEq(entity.ID(clusterID)).One(&cluster)
	})
	if err != nil {
		return nil, err
	}

	params := []grafana.SearchParam{
		grafana.SearchType(grafana.SearchTypeDashboard),
		grafana.SearchQuery(""),
		grafana.SearchStarred(false),
	}
	params = append(params, grafana.SearchTag(config.Global().GrafanaConfig.TidbDashboardTag))
	items, err := m.grafanaClient.Search(context.TODO(), params...)
	if err != nil {
		return nil, err
	}
	grafanaServer := strings.TrimSuffix(config.Global().GrafanaConfig.Server, "/")
	var ds []DashboardLink
	for _, item := range items {
		ds = append(ds, DashboardLink{Name: item.Title, Link: fmt.Sprintf("%s%s?%s", grafanaServer, item.URL, buildGrafanaDashboardURLQueryParameter(t.Name, cluster.Name, tenantID, clusterID))})
	}
	return ds, nil
}

func buildGrafanaDashboardURLQueryParameter(tenantName, clusterName string, tenantID, clusterID uint64) string {
	// FIXME: Currently this is completely copied from admin model.
	u := url.URL{}
	q := u.Query()
	q.Add("var-tenant_name", tenantName)
	q.Add("var-tenant_id", fmt.Sprintf("%d", tenantID))
	q.Add("var-cluster_name", clusterName)
	q.Add("var-cluster_id", fmt.Sprintf("%d", clusterID))
	return q.Encode()
}
