package main

import (
	"bytes"
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	cfg "github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/commons/database"

	cconf "github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/internel/dao/entity"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/schema"
)

type Output struct {
	buffer *bytes.Buffer
}

func NewOutput() *Output {
	o := &Output{
		buffer: bytes.NewBuffer(nil),
	}
	return o
}

func (o *Output) Printf(format string, args ...interface{}) {
	fmt.Fprintf(o.buffer, format, args...)
	fmt.Fprintf(os.Stdout, format, args...)
}

func (o *Output) AllOutput() string {
	return o.buffer.String()
}

func showBanner(format string, args ...interface{}) {
	log.Printf("+ "+format+"\n", args...)
}

func connectDatabase(dsn string) (*gorm.DB, error) {
	opts := []gorm.Option{
		&gorm.Config{
			NamingStrategy: schema.NamingStrategy{
				NameReplacer: strings.NewReplacer("OpenAPI", "openapi"),
			},
		},
	}

	db, err := gorm.Open(mysql.Open(dsn), opts...)
	if err != nil {
		return nil, err
	}

	return db, nil
}

func parseExpectedResult(s string) []string {
	result := make([]string, 0)
	for _, x := range strings.Split(s, "/") {
		if len(x) > 0 {
			result = append(result, x)
		}
	}

	return result
}

type JArray []interface{}

type JObject map[string]interface{}

func SendFeishuMessage(hookID string, title string, text string) {
	url := fmt.Sprintf("https://open.feishu.cn/open-apis/bot/v2/hook/%s", hookID)
	body := JObject{
		"msg_type": "post",
		"content": JObject{
			"post": JObject{
				"zh_cn": JObject{
					"title": title,
					"content": JArray{
						JArray{
							JObject{
								"tag":  "text",
								"text": text,
							},
						},
					},
				},
			},
		},
	}

	bodyBytes, _ := json.Marshal(body)
	request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(bodyBytes))
	if err != nil {
		panic(err)
	}

	client := http.Client{}
	response, err := client.Do(request)
	if err != nil {
		panic(err)
	}

	output, err := ioutil.ReadAll(response.Body)
	if err != nil {
		panic(err)
	}

	fmt.Printf("OUTPUT: %s\n", output)
}

type CheckConfig struct {
	Watch    *Target
	Expected []string
	DoFix    bool
	Output   string
	Env      string
	HookID   string
}

func runChecker(db *Database, conf *CheckConfig) {
	checker := NewRoleChecker()
	out := NewOutput()
	doctor := NewDoctor(out)

	showBanner("reading role bindings from database")
	roles, err := db.ReadRoleBindings()
	if err != nil {
		panic(err)
	}
	showBanner("adding %d role bindings", len(roles))
	checker.AddRoles(roles)

	showBanner("reading users from database")
	users, err := db.ReadUsers()
	if err != nil {
		panic(err)
	}
	showBanner("adding %d users", len(users))
	checker.AddUsers(users)
	doctor.AddUsers(users)

	showBanner("reading user-project bindings from database")
	ups, err := db.ReadUserProjectBindings()
	if err != nil {
		panic(err)
	}
	showBanner("adding %d user-project bindings", len(ups))
	checker.AddProjects(ups)

	showBanner("expected result status: %v", conf.Expected)
	showBanner("do data fix: %v", conf.DoFix)

	out.Printf("run check on %s\n", time.Now().Format(time.RFC3339))
	if len(conf.Env) > 0 {
		out.Printf("run on env %s\n", conf.Env)
	}

	total, success := 0, 0
	if conf.Watch != nil {
		showBanner("watch on %+v", conf.Watch)
		r := checker.ReportOn(out, *conf.Watch, conf.Expected, doctor.FixCallback)
		if r {
			success = 1
		}
		total = 1

	} else {
		total, success = checker.Report(out, conf.Expected, doctor.FixCallback)
	}

	output := out.buffer.Bytes()
	if len(conf.Output) > 0 {
		fd, err := os.Create(conf.Output)
		if err != nil {
			fmt.Printf("create output file ERROR: %s\n", err)
			return
		}
		defer func() {
			_ = fd.Close()
		}()

		_, err = fd.Write(output)
		if err != nil {
			fmt.Printf("write output file ERROR: %s\n", err)
		}
	}

	if len(conf.HookID) > 0 {
		title := fmt.Sprintf("[%s] check on env=%s result=%d/%d",
			time.Now().Format("2006-01-02T15:04"), conf.Env, success, total)
		SendFeishuMessage(conf.HookID, title, string(output))
	}
}

func main() {
	dsn := flag.String("db", "root:@/account?parseTime=true", "database connection string")
	config := flag.String("config", "", "central config file")
	watchType := flag.String("type", "", "Watch result in target type")
	watchID := flag.Uint64("id", 0, "Watch result in target id")
	doFix := flag.Bool("fix", false, "Do database operations to fix")
	output := flag.String("output", "", "write output to file")
	env := flag.String("env", "", "environment name")
	hookID := flag.String("hook", "", "hook id")
	result := flag.String("result", CheckResultDualWriteOK,
		fmt.Sprintf("expected result, can be one of %s/%s/%s/%s",
			CheckResultDualWriteOK, CheckResultMissing, CheckResultPolyOrgOK, CheckResultConflict))
	flag.Parse()

	var err error
	var rawDB *gorm.DB
	if len(*config) > 0 {
		fmt.Printf("connect database with config file %s\n", *config)
		cconf.SetConfigPath(*config)
		newConfig, _ := cfg.NewConfig()
		rawDB, _ = database.Initialize(context.Background(), newConfig.MysqlConfig)

	} else {
		fmt.Printf("connect database with dsn %s\n", *dsn)
		rawDB, err = connectDatabase(*dsn)
	}

	if err != nil {
		panic(err)
	}

	conf := &CheckConfig{
		Expected: parseExpectedResult(*result),
		DoFix:    *doFix,
		Output:   *output,
		Env:      *env,
		HookID:   *hookID,
	}

	if len(*watchType) > 0 && *watchID > 0 {
		conf.Watch = &Target{
			Type: entity.TargetType(*watchType),
			ID:   entity.ID(*watchID),
		}
	}

	db := NewDatabase(rawDB)
	runChecker(db, conf)
}
