package resource

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	prometheus "github.com/prometheus/client_golang/api"
	prometheus_api "github.com/prometheus/client_golang/api/prometheus/v1"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/core"
	logsearch "github.com/pingcap/test-infra/sdk/pkg/logs"
	"github.com/pingcap/test-infra/sdk/pkg/util/pdutil"
)

type TiDBAddrType int

const (
	PDAddr TiDBAddrType = iota
	KVAddr
	KVStatus
	DBAddr
	DBStatus
	CDCAddr
	PDApi
	TiFlashAddr
	Prometheus
	Alertmanager
	Grafana
)

func (t TiDBAddrType) String() string {
	switch t {
	case PDAddr:
		return "pd-addr"
	case KVAddr:
		return "kv-addr"
	case KVStatus:
		return "kv-status"
	case DBAddr:
		return "db-addr"
	case DBStatus:
		return "db-status"
	case CDCAddr:
		return "cdc-addr"
	case PDApi:
		return "pd-api"
	case TiFlashAddr:
		return "flash-addr"
	default:
		return "#" + strconv.Itoa(int(t))
	}
}

const (
	PD TiDBCompType = iota
	TiKV
	TiDB
	TiCDC
	TiFlash
)

type TiDBCompType int

func (_ TiDBCompType) FromString(str string) (TiDBCompType, error) {
	switch str {
	case "pd":
		return PD, nil
	case "tikv":
		return TiKV, nil
	case "tidb":
		return TiDB, nil
	case "ticdc":
		return TiCDC, nil
	case "tiflash":
		return TiFlash, nil
	default:
		return -1, errors.New("unsupported TiDBCompType")
	}
}

func (t TiDBCompType) String() string {
	switch t {
	case PD:
		return "pd"
	case TiKV:
		return "tikv"
	case TiDB:
		return "tidb"
	case TiCDC:
		return "ticdc"
	case TiFlash:
		return "tiflash"
	default:
		return "#" + strconv.Itoa(int(t))
	}
}

type Addressable interface {
	ServiceURL(typ TiDBAddrType) (*url.URL, error)
	PeerURLs(typ TiDBAddrType) ([]*url.URL, error)
}

// TiDBClusterTiDBSelectorSpec specifies how to filter the tidb-server instances.
//
// Note that can only set one field one time
//
// For example, to select the first instance:
//  Peer: 0
// Or select alls:
//  All: true
type TiDBClusterTiDBSelectorSpec struct {
	Peer *int
	All  bool
}

// TiDBClusterTiKVSelectorSpec specifies how to filter the tikv-server instances.
//
// Note that can only set one field one time
//
// For example, to select the first instance:
//  Peer: 0
// Or select the only leader instance:
//  StoreLabelSelector: &metav1.LabelSelector{
// 		MatchLabels: map[string]string{
// 			"zone": "zone3",
// 		},
// },
// Or select alls:
//  All: true
type TiDBClusterTiKVSelectorSpec struct {
	Peer *int
	// TODO mahjonp: create a LabelSelector
	//  mysql> select ADDRESS, LABEL from INFORMATION_SCHEMA.TIKV_STORE_STATUS;
	//  +-------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+
	//  | ADDRESS                                                     | LABEL                                                                                                            |
	//  +-------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+
	//  | tc1-tikv-1.tc1-tikv-peer.testbed-h2r9w.svc:20160      | [{"key": "host", "value": "idc-4-26"}, {"key": "zone", "value": "zone1"}]                                        |
	//  | tc1-tikv-0.tc1-tikv-peer.testbed-h2r9w.svc:20160      | [{"key": "host", "value": "idc-4-28"}, {"key": "zone", "value": "zone2"}]                                        |
	//  | tc1-tikv-2.tc1-tikv-peer.testbed-h2r9w.svc:20160      | [{"key": "host", "value": "idc-4-72"}, {"key": "zone", "value": "zone3"}]                                        |
	//  | tc1-tiflash-0.tc1-tiflash-peer.testbed-h2r9w.svc:3930 | [{"key": "engine", "value": "tiflash"}, {"key": "zone", "value": "zone3"}, {"key": "host", "value": "idc-4-71"}] |
	//  +-------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+
	//  4 rows in set (0.05 sec)
	StoreLabelSelector *v1.LabelSelector
	All                bool
}

// TiDBClusterTiFlashSelectorSpec specifies how to filter the tikv-server instances.
//
// Note that can only set one field one time
//
// For example, to select the first instance:
//  Peer: 0
// Or select the only leader instance:
//  StoreLabelSelector: &metav1.LabelSelector{
// 		MatchLabels: map[string]string{
// 			"zone": "zone3",
// 		},
// },
// Or select alls:
//  All: true
type TiDBClusterTiFlashSelectorSpec struct {
	Peer *int
	// TODO mahjonp: create a LabelSelector
	//  mysql> select ADDRESS, LABEL from INFORMATION_SCHEMA.TIKV_STORE_STATUS;
	//  +-------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+
	//  | ADDRESS                                                     | LABEL                                                                                                            |
	//  +-------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+
	//  | tc1-tikv-1.tc1-tikv-peer.testbed-h2r9w.svc:20160      | [{"key": "host", "value": "idc-4-26"}, {"key": "zone", "value": "zone1"}]                                        |
	//  | tc1-tikv-0.tc1-tikv-peer.testbed-h2r9w.svc:20160      | [{"key": "host", "value": "idc-4-28"}, {"key": "zone", "value": "zone2"}]                                        |
	//  | tc1-tikv-2.tc1-tikv-peer.testbed-h2r9w.svc:20160      | [{"key": "host", "value": "idc-4-72"}, {"key": "zone", "value": "zone3"}]                                        |
	//  | tc1-tiflash-0.tc1-tiflash-peer.testbed-h2r9w.svc:3930 | [{"key": "engine", "value": "tiflash"}, {"key": "zone", "value": "zone3"}, {"key": "host", "value": "idc-4-71"}] |
	//  +-------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------+
	//  4 rows in set (0.05 sec)
	StoreLabelSelector *v1.LabelSelector
	All                bool
}

// TiDBClusterPDSelectorSpec specifies how to filter the pd-server instances.
//
// Note that can only set one field one time
//
// For example, to select the first instance:
//  Peer: 0
// Or select the only leader instance:
//  IsLeader: &[]bool{true, false}[0]
// Or select all:
//  All: true
type TiDBClusterPDSelectorSpec struct {
	// Peer specifies the pd-server instance
	Peer *int
	// IsLeader filters leader or not leader pd-server instances
	IsLeader *bool
	// All selects all pd-server instances
	All bool
}

// TiDBClusterCDCSelectorSpec specifies how to filter the cdc-server instances
//
// Note that can only set one field one time
//
// For example, to select the first instance:
//  Peer: 0
// Or select the only owner instance:
//  IsOwner: &[]bool{true, false}[0]
// Or select all:
//  All: true
type TiDBClusterCDCSelectorSpec struct {
	// Peer specifies the cdc-server instance
	Peer *int
	// IsOwner filters leader or not cdc-server instance
	IsOwner *bool
	// RandomSameOne selects a random cdc-server instance
	RandomSameOne bool
	// All selects all cdc-server instances
	All bool
}

// TiDBClusterSelectorOpts is the instance selector options.
//
// For example to select the pd leader:
// 	resource.TiDBClusterSelectorOpts{
//		TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
//			IsLeader: &[]bool{true, false}[0],
//		},
//	}
//
// Select all tidbs and pds:
//  resource.TiDBClusterSelectorOpts{
//		TiDBClusterPDSelectorSpec: &resource.TiDBClusterPDSelectorSpec{
//			All: true,
//		},
//		TiDBClusterTiDBSelectorSpec: &resource.TiDBClusterTiDBSelectorSpec{
//			All: true,
//		},
// }
type TiDBClusterSelectorOpts struct {
	*TiDBClusterTiDBSelectorSpec
	*TiDBClusterTiKVSelectorSpec
	*TiDBClusterPDSelectorSpec
	*TiDBClusterTiFlashSelectorSpec
	*TiDBClusterCDCSelectorSpec
}

type TiDBClusterOOMWatchOptions struct {
	Target TiDBCompType
}

type OOMEvent struct {
	InstanceName string
	InstanceIP   string
	HappenedAt   time.Time
}

// TiDBComponentNode is the tidb component, tikv, pd, ticdc
type TiDBComponentNode interface {
	Restart() error
	CleanData() error
}

// TiDBCluster is the tidb cluster interface
type TiDBCluster interface {
	core.Resource
	MySQLCompatible
	Exec(opts TiDBClusterExecOptions) (stdout string, stderr string, exitCode int, err error)
	UpdateConfig(opts TiDBClusterUpdateConfigOptions) error
	UpdateConfigs(opts TiDBClusterUpdateConfigsOptions) error
	Upgrade(opts TiDBClusterUpgradeOptions) error
	Scale(opts TiDBClusterScaleOptions) error
	SearchLog(ctx context.Context, opts TiDBClusterSearchLogOptions) ([]string, error)
	GetSpec(comp TiDBCompType) (*apiresource.TiDBClusterComponentSpec, error)
	GetStatus(comp TiDBCompType) (*apiresource.TiDBClusterComponentStatus, error)
	WatchOOM(ctx context.Context, opts TiDBClusterOOMWatchOptions) (<-chan OOMEvent, error)
	ChaosSelect(selector TiDBClusterSelectorOpts) ([]ChaosSelector, error)
	ChaosClient() ChaosClient
	PrometheusClient() (prometheus.Client, error)
	PrometheusAPI() (prometheus_api.API, error)
	PDClient() (*pdutil.Client, error)
	GetNodes(TiDBCompType) ([]TiDBComponentNode, error)
	// WriteFile(opts TiDBClusterWriteFileOptions) error
}

// TiDBClusterExecOptions wraps a command which wants to execute on target component node
type TiDBClusterExecOptions struct {
	Target  TiDBCompType
	Command string
	Peer    int
}

// TiDBClusterUpdateConfigOptions wraps config which needs to be applied to a component
//
// For example to update a component's config:
// 	resource.TiDBClusterUpdateConfigOptions{
// 		Target:  sdkresource.TiKV,
// 		Config:  "[rocksdb]\nmax-open-files = 128\n[storage]\nreserve-space = \"150MB\"",
// 		Timeout: <int> * time.Minute/time.Hour,
// 	}
type TiDBClusterUpdateConfigOptions struct {
	Target TiDBCompType
	// New configs that will override with the old config to a component
	Config string
	// Timeout wait for updating config, a timeout of 0 is interpreted as an infinity
	Timeout time.Duration
}

// TiDBClusterUpdateConfigsOptions wraps configs which needs to be applied to components
//
// For example to update tidb and tikv configs:
//   TiDBClusterUpdateConfigsOptions{
//   	Configs: map[TiDBCompType]string{TiKV: "[rocksdb]\\nmax-open-files = 128\\n[storage]\\nreserve-space = \\\"150MB\\\"", TiDB: "[log]\nlevel = \"debug\""},
//   	Timeout: 5*time.Minute,
//   }
type TiDBClusterUpdateConfigsOptions struct {
	// New configs that will override with the old config to components
	Configs map[TiDBCompType]string
	// Timeout wait for updating config, a timeout of 0 is interpreted as an infinity
	Timeout time.Duration
}

// TiDBClusterUpgradeOptions wraps a version wants to upgrade to on target component node
type TiDBClusterUpgradeOptions struct {
	// ComponentUpgradeOption is used to specify the single component to upgrade, cannot set ComponentUpgradeOption with Version the same time
	ComponentUpgradeOption *TiDBClusterComponentUpgradeOption

	// Version is like: v5.2.0 style or release-5.0, master, nightly etc.
	//
	// For K8s, the full image for a component is: <pd/tidb/tikv/pump/tiflash/ticdc>.baseImage + version
	//
	// For TiUP, only the Version field is needed
	Version string
	// OverrideComponentsUpgradeOption supports a way to set the specific component version(eg: the hotfix upgrade scenario)
	OverrideComponentsUpgradeOption []TiDBClusterComponentUpgradeOption

	// Timeout wait for scaling, a timeout of 0 is interpreted as an infinity
	// Remember that the timeout is affected by the duration of tikv EvictLeaderTimeout, don't forget to set the spec.tikv.EvictLeaderTimeout on the testbed
	Timeout time.Duration
}

type TiDBClusterComponentUpgradeOption struct {
	Target TiDBCompType
	// Version is like: v5.2.0 style or release-5.0, master, nightly etc.
	//
	// For K8s, the full image for a component is: <pd/tidb/tikv/pump/tiflash/ticdc>.baseImage + version
	//
	// For TiUP, only the Version field is needed
	Version string
}

type TiDBClusterScaleOptions struct {
	Target TiDBCompType
	// Replicas the replica number expect
	Replicas uint32
	// Timeout wait for scaling, a timeout of 0 is interpreted as an infinity
	Timeout time.Duration
}

type TiDBClusterSearchLogOptions struct {
	Target TiDBCompType
	// LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelTrace, LogLevelCritical, LogLevelError
	Levels             []logsearch.LogLevel
	StartTime, EndTime time.Time
	Patterns           []string
	// Limits set the total line count returns
	Limit int
}

// TODO(@mahjonp): put on the second milestone
// type TiDBClusterSetConfigOptions struct {
//	Target   TiDBCompType
//	Config   string
//	Deferred bool
// }
//
// type TiDBClusterWriteFileOptions struct {
//	Target  TiDBCompType
//	Content io.Reader
//	Path    string
//	Peer    int
// }

func BuildDSN(a *url.URL, db string, params ...string) string {
	user, pass := "root", ""
	if a.User != nil {
		user = a.User.Username()
		pass, _ = a.User.Password()
	}
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, a.Hostname(), a.Port(), db)
	if len(params) > 0 {
		dsn += "?" + strings.Join(params, "&")
	}
	return dsn
}
