/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package diagnose

import (
	"bytes"
	"context"
	"fmt"

	"github.com/apache/servicecomb-service-center/client"
	"github.com/apache/servicecomb-service-center/pkg/dump"
	"github.com/apache/servicecomb-service-center/scctl/etcd"
	"github.com/apache/servicecomb-service-center/scctl/pkg/cmd"
	"github.com/spf13/cobra"
	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
)

const (
	service  = "service"
	instance = "instance"
)

const (
	greater = iota
	mismatch
	less
)

var typeMap = map[string]string{
	service:  "/cse-sr/ms/files/",
	instance: "/cse-sr/inst/files/",
}

type etcdResponse map[string][]*mvccpb.KeyValue

func CommandFunc(_ *cobra.Command, _ []string) {
	// initialize sc/etcd clients
	scClient, err := client.NewSCClient(cmd.ScClientConfig)
	if err != nil {
		cmd.StopAndExit(cmd.ExitError, err)
	}
	etcdClient, err := etcd.NewEtcdClient(EtcdClientConfig)
	if err != nil {
		cmd.StopAndExit(cmd.ExitError, err)
	}
	defer etcdClient.Close()

	// query etcd
	etcdResp, err := getEtcdResponse(context.Background(), etcdClient)
	if err != nil {
		cmd.StopAndExit(cmd.ExitError, err)
	}

	// query sc
	cache, scErr := scClient.GetScCache(context.Background())
	if scErr != nil {
		cmd.StopAndExit(cmd.ExitError, scErr)
	}

	// diagnose go...
	details, err := diagnose(cache, etcdResp)
	if err != nil {
		fmt.Println(details)                // stdout
		cmd.StopAndExit(cmd.ExitError, err) // stderr
	}
}

func getEtcdResponse(ctx context.Context, etcdClient *clientv3.Client) (etcdResponse, error) {
	etcdResp := make(etcdResponse)
	for t, prefix := range typeMap {
		if err := setResponse(ctx, etcdClient, t, prefix, etcdResp); err != nil {
			return nil, err
		}
	}
	return etcdResp, nil
}

func setResponse(ctx context.Context, etcdClient *clientv3.Client, key, prefix string, etcdResp etcdResponse) error {
	resp, err := etcdClient.Get(ctx, prefix, clientv3.WithPrefix())
	if err != nil {
		return err
	}
	etcdResp[key] = resp.Kvs
	return nil
}

func diagnose(cache *dump.Cache, etcdResp etcdResponse) (details string, err error) {
	var (
		service  = ServiceCompareHolder{Cache: cache.Microservices, Kvs: etcdResp[service]}
		instance = InstanceCompareHolder{Cache: cache.Instances, Kvs: etcdResp[instance]}
	)

	sr := service.Compare()
	ir := instance.Compare()

	var (
		b    bytes.Buffer
		full bytes.Buffer
	)
	writeResult(&b, &full, sr, ir)
	if b.Len() > 0 {
		return full.String(), fmt.Errorf("error: %s", b.String())
	}
	return "", nil
}

func writeResult(b *bytes.Buffer, full *bytes.Buffer, rss ...*CompareResult) {
	g, m, l := make(map[string][]string), make(map[string][]string), make(map[string][]string)
	for _, rs := range rss {
		for t, arr := range rs.Results {
			switch t {
			case greater:
				g[rs.Name] = arr
			case mismatch:
				m[rs.Name] = arr
			case less:
				l[rs.Name] = arr
			}
		}
	}

	i := 0
	if s := len(g); s > 0 {
		i++
		header := fmt.Sprintf("%d. found in cache but not in etcd ", i)
		b.WriteString(header)
		full.WriteString(header)
		writeBody(full, g)
	}
	if s := len(m); s > 0 {
		i++
		header := fmt.Sprintf("%d. found different between cache and etcd ", i)
		b.WriteString(header)
		full.WriteString(header)
		writeBody(full, m)
	}
	if s := len(l); s > 0 {
		i++
		header := fmt.Sprintf("%d. found in etcd but not in cache ", i)
		b.WriteString(header)
		full.WriteString(header)
		writeBody(full, l)
	}
	if l := b.Len(); l > 0 {
		b.Truncate(l - 1)
		full.Truncate(full.Len() - 1)
	}
}

func writeBody(b *bytes.Buffer, r map[string][]string) {
	b.WriteString("\b, details:\n")
	for t, v := range r {
		writeSection(b, t)
		b.WriteString(fmt.Sprint(v))
		b.WriteRune('\n')
	}
}

func writeSection(b *bytes.Buffer, t string) {
	b.WriteString("  ")
	b.WriteString(t)
	b.WriteString(": ")
}
