// Copyright 2020 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package instance

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/pingcap/errors"
	"github.com/pingcap/tiup/pkg/tidbver"
	"github.com/pingcap/tiup/pkg/tui/colorstr"
	"github.com/pingcap/tiup/pkg/utils"
)

// TiFlashRole is the role of TiFlash.
type TiFlashRole = string

const (
	// TiFlashRoleNormal is used when TiFlash is not in disaggregated mode.
	TiFlashRoleNormal TiFlashRole = "normal"

	// TiFlashRoleDisaggWrite is used when TiFlash is in disaggregated mode and is the write node.
	TiFlashRoleDisaggWrite TiFlashRole = "write"
	// TiFlashRoleDisaggCompute is used when TiFlash is in disaggregated mode and is the compute node.
	TiFlashRoleDisaggCompute TiFlashRole = "compute"
)

// TiFlashInstance represent a running TiFlash
type TiFlashInstance struct {
	instance
	shOpt           SharedOptions
	tcpPort         int
	servicePort     int
	proxyPort       int
	proxyStatusPort int
	pds             []*PDInstance
	dbs             []*TiDBInstance
}

var _ Instance = &TiFlashInstance{}

// NewTiFlashInstance return a TiFlashInstance
func NewTiFlashInstance(role TiFlashRole, shOpt SharedOptions, binPath, dir, host, configPath string, id int, pds []*PDInstance, dbs []*TiDBInstance, version string) *TiFlashInstance {
	if role != TiFlashRoleNormal && role != TiFlashRoleDisaggWrite && role != TiFlashRoleDisaggCompute {
		panic(fmt.Sprintf("Unknown TiFlash role %s", role))
	}
	if (role == TiFlashRoleDisaggCompute || role == TiFlashRoleDisaggWrite) && shOpt.Mode != ModeCSE && shOpt.Mode != ModeDisAgg && shOpt.Mode != ModeNextGen {
		panic(fmt.Sprintf("Unsupported disagg role in mode %s", shOpt.Mode))
	}

	httpPort := 8123
	if !tidbver.TiFlashNotNeedHTTPPortConfig(version) {
		httpPort = utils.MustGetFreePort(host, httpPort, shOpt.PortOffset)
	}
	return &TiFlashInstance{
		shOpt: shOpt,
		instance: instance{
			BinPath:    binPath,
			ID:         id,
			Dir:        dir,
			Host:       host,
			Port:       httpPort,
			StatusPort: utils.MustGetFreePort(host, 8234, shOpt.PortOffset),
			ConfigPath: configPath,
			role:       role,
		},
		tcpPort:         utils.MustGetFreePort(host, 9100, shOpt.PortOffset), // 9000 for default object store port
		servicePort:     utils.MustGetFreePort(host, 3930, shOpt.PortOffset),
		proxyPort:       utils.MustGetFreePort(host, 20170, shOpt.PortOffset),
		proxyStatusPort: utils.MustGetFreePort(host, 20292, shOpt.PortOffset),
		pds:             pds,
		dbs:             dbs,
	}
}

// Addr return the address of tiflash
func (inst *TiFlashInstance) Addr() string {
	return utils.JoinHostPort(AdvertiseHost(inst.Host), inst.servicePort)
}

// MetricAddr implements Instance interface.
func (inst *TiFlashInstance) MetricAddr() (r MetricAddr) {
	r.Targets = append(r.Targets, utils.JoinHostPort(inst.Host, inst.StatusPort))
	r.Targets = append(r.Targets, utils.JoinHostPort(inst.Host, inst.proxyStatusPort))
	return
}

// Start calls set inst.cmd and Start
func (inst *TiFlashInstance) Start(ctx context.Context) error {
	// If inst.Version does not meet the new start mode, use old start mode for backward compatibility.
	// If inst.Version is empty, it means the tiflash is started by specific binary path.
	// Usually this case is use for testing or development, start with new mode.
	if !inst.Version.IsEmpty() && !tidbver.TiFlashPlaygroundNewStartMode(inst.Version.String()) {
		colorstr.Printf("[yellow][bold]Warning[reset][bold]: TiFlash is started by a compatibility method for versions earlier than v7.1. [reset]\n")
		return inst.startOld(ctx, inst.Version)
	}

	proxyConfigPath := filepath.Join(inst.Dir, "tiflash_proxy.toml")
	if err := prepareConfig(
		proxyConfigPath,
		"",
		inst.getProxyConfig(),
	); err != nil {
		return err
	}

	configPath := filepath.Join(inst.Dir, "tiflash.toml")
	if err := prepareConfig(
		configPath,
		inst.ConfigPath,
		inst.getConfig(),
	); err != nil {
		return err
	}

	endpoints := pdEndpoints(inst.pds, false)

	args := []string{
		"server",
		fmt.Sprintf("--config-file=%s", configPath),
		"--",
	}
	runtimeConfig := [][]string{
		{"path", filepath.Join(inst.Dir, "data")},
		{"listen_host", inst.Host},
		{"logger.log", inst.LogFile()},
		{"logger.errorlog", filepath.Join(inst.Dir, "tiflash_error.log")},
		{"status.metrics_port", fmt.Sprintf("%d", inst.StatusPort)},
		{"flash.service_addr", utils.JoinHostPort(AdvertiseHost(inst.Host), inst.servicePort)},
		{"raft.pd_addr", strings.Join(endpoints, ",")},
		{"flash.proxy.addr", utils.JoinHostPort(inst.Host, inst.proxyPort)},
		{"flash.proxy.advertise-addr", utils.JoinHostPort(AdvertiseHost(inst.Host), inst.proxyPort)},
		{"flash.proxy.status-addr", utils.JoinHostPort(inst.Host, inst.proxyStatusPort)},
		{"flash.proxy.data-dir", filepath.Join(inst.Dir, "proxy_data")},
		{"flash.proxy.log-file", filepath.Join(inst.Dir, "tiflash_tikv.log")},
	}
	userConfig, err := unmarshalConfig(configPath)
	if err != nil {
		return errors.Trace(err)
	}
	for _, arg := range runtimeConfig {
		// if user has set the config, skip it
		if !isKeyPresentInMap(userConfig, arg[0]) {
			args = append(args, fmt.Sprintf("--%s=%s", arg[0], arg[1]))
		}
	}

	return inst.PrepareProcess(ctx, inst.BinPath, args, nil, inst.Dir)
}

func isKeyPresentInMap(m map[string]any, key string) bool {
	keys := strings.Split(key, ".")
	currentMap := m

	for i := range keys {
		if _, ok := currentMap[keys[i]]; !ok {
			return false
		}

		// If the current value is a nested map, update the current map to the nested map
		if innerMap, ok := currentMap[keys[i]].(map[string]any); ok {
			currentMap = innerMap
		}
	}

	return true
}

// Component return the component name.
func (inst *TiFlashInstance) Component() string {
	return "tiflash"
}

// LogFile return the log file name.
func (inst *TiFlashInstance) LogFile() string {
	return filepath.Join(inst.Dir, "tiflash.log")
}

// StoreAddr return the store address of TiFlash
func (inst *TiFlashInstance) StoreAddr() string {
	return utils.JoinHostPort(AdvertiseHost(inst.Host), inst.servicePort)
}
