package main

import (
	"fmt"
	"sort"
	"strconv"
	"strings"
)

type checkSummary struct {
	dss []dbCheckSummary
}
type dbCheckSummary struct {
	db   string
	desc string //has content,mean is skip.
	tss  []tblCheckSummary
}

type tblCheckSummary struct {
	desc  string //has content,mean is skip.
	tbl   string
	total int
	rs    []Range //后期可能要自动处理这些失败的.这里仅仅打印下日志.
}

func (s *checkSummary) String() string {
	var bs strings.Builder
	bs.WriteString(fmt.Sprintf("\n\nSummary:\n"))
	dbSpace := " |-"
	tblSpace := "   |-"
	rsSpace := "     |-"
	for _, ds := range s.dss {
		bs.WriteString(dbSpace)
		bs.WriteString(ds.db)
		if ds.desc != "" {
			bs.WriteString(" error:")
			bs.WriteString(ds.desc)
			bs.WriteByte('\n')
			//skip;
			continue
		}
		bs.WriteString(fmt.Sprintf(" %d tables:\n", len(ds.tss)))
		for _, ts := range ds.tss {
			bs.WriteString(tblSpace)
			bs.WriteString(ts.tbl)
			if ts.desc != "" {
				bs.WriteString(" error:")
				bs.WriteString(ts.desc)
				bs.WriteByte('\n')
				//skip;
				continue
			}
			bs.WriteString(fmt.Sprintf(" split %d range,", ts.total))
			if len(ts.rs) == 0 {
				bs.WriteString("all success.\n")
				continue
			}
			//errors;
			bs.WriteString(fmt.Sprintf(" failed %d range:\n", len(ts.rs)))
			for _, r := range ts.rs {
				bs.WriteString(rsSpace)
				bs.WriteString(fmt.Sprintf("%+v\n", r))
			}
		}
	}
	return bs.String()
}

func doCheckDiff(pgc, tidbc dbCfg, cd checkdiff) {
	nthread, limit, dbs := cd.NThread, cd.Limit, cd.DBs
	var sum checkSummary
	for _, db := range dbs {
		tidb, err := makeTidbDatabase(tidbc, db)
		if err != nil {
			LogErr(err.Error())
			return
		}
		pg, err := makePGDatabase(pgc, db)
		if err != nil {
			LogErr(err.Error())
			return
		}

		diffInfo := checkDiffInfo{
			pg:     pg,
			tidb:   tidb,
			parall: nthread,
		}
		ds := checkDiffDB(diffInfo, limit)
		ds.db = db
		sum.dss = append(sum.dss, ds)
	}
	LogErr(sum.String())
}

func checkDiffDB(cdi checkDiffInfo, limit int) (ds dbCheckSummary) {
	db := cdi.pg.dbname
	tbls, err := GetTableListByDB(cdi.pg, base_type)
	if err != nil {
		LogErr("GetTableListByDB(%s) err:%s. ", db, err.Error())
		ds.desc = fmt.Sprintf("GetTableListByDB(%s) err:%s. ", db, err.Error())
		return
	}
	for _, tbl := range tbls {
		ts := checkDiffTbl(cdi, tbl, limit)
		ds.tss = append(ds.tss, ts)
	}
	return ds
}

func checkDiffTbl(cdi checkDiffInfo, tbl string, limit int) tblCheckSummary {
	db := cdi.pg.dbname
	var tblSum tblCheckSummary
	tblSum.tbl = tbl
	pk, err := findPrimaryKey(cdi.pg, tbl)
	if err != nil {
		if err == ErrNoPrimaryKey {
			tblSum.desc = fmt.Sprintf("'%s.%s' was no primary key.not support", db, tbl)
			LogErr(tblSum.desc)
			return tblSum
		}
		tblSum.desc = fmt.Sprintf("'%s.%s' findPrimaryKey err:%s", db, tbl, err.Error())
		LogErr(tblSum.desc)
		return tblSum
	}
	//get from tidb;quick.
	cdi.rs, err = SplitChunk(cdi.pg.db, tbl, limit, pk)
	if err != nil {
		tblSum.desc = fmt.Sprintf("%s.%s SplitChunk err:%s", db, tbl, err.Error())
		LogErr(tblSum.desc)
		return tblSum
	}
	cdi.tbl = tbl
	errs, skipTable := CheckDiffByRanges(cdi)
	tblSum.total = len(cdi.rs)
	if skipTable {
		tblSum.desc = fmt.Sprintf("'%s.%s' checkDiffDB check too many erros.skip it and do check it latter.", db, tbl)
		return tblSum
	} else {
		if len(errs) > 0 {
			//tblS.desc = fmt.Sprintf("'%s.%s' checkDiffDB check erros %d:\n", db, tbl, len(errs))
			for _, ei := range errs {
				tblSum.rs = append(tblSum.rs, cdi.rs[ei])
			}
		}
		return tblSum
	}
}

func checkDiffRange(pgc, tidbc dbCfg, cdr checkRange) {
	db := cdr.DB
	tidb, err := makeTidbDatabase(tidbc, db)
	if err != nil {
		LogErr(err.Error())
		return
	}
	pg, err := makePGDatabase(pgc, db)
	if err != nil {
		LogErr(err.Error())
		return
	}
	diffInfo := checkDiffInfo{
		pg:     pg,
		tidb:   tidb,
		parall: cdr.NThread,
		tbl:    cdr.Tbl,
	}
	var rs []Range
	for _, cr := range cdr.Ranges {
		rs = append(rs, Range{
			cr.Start,
			cr.End,
			cdr.Count,
		})
	}
	tblSum, ids := loopCheckRanges(diffInfo, rs)
	//summary.
	var b strings.Builder
	b.WriteString(fmt.Sprintf("\n\nSummary:\n"))
	b.WriteString(fmt.Sprintf(" '%s.%s' :\n", cdr.DB, cdr.Tbl))
	if len(tblSum.desc) > 0 {
		b.WriteString("    recheck err:tblSum.desc.\n")
	}
	if len(tblSum.rs) > 0 {
		b.WriteString(fmt.Sprintf("  ranges:%+v", tblSum.rs))
	}
	if len(ids) > 0 {
		b.WriteString("  Ids:\n")
		showIds(ids, &b, "    ")
	}
	LogErr(b.String())
}

func showIds(ids IDsT, b *strings.Builder, indent string) {
	_, e := strconv.ParseInt(string(ids[0]), 10, 64)
	if e != nil {
		b.WriteString(indent)
		for idx, s := range ids {
			b.WriteString(string(s))
			b.WriteByte(';')
			if idx < 10 {
				b.WriteString(fmt.Sprintf("(has '%d' element)", len(ids)))
				return
			}
		}
		return
	}
	//number
	var ivs = make([]int, len(ids))
	for i := 0; i < len(ids); i++ {
		ivs[i], _ = strconv.Atoi(string(ids[i]))
	}
	sort.Ints(ivs)
	start := 0
	prv := ivs[start]
	b.WriteString(indent)
	for i := 1; i < len(ids); i++ {
		if prv != ivs[i]-1 {
			b.WriteString(fmt.Sprintf("%d->%d(%d)\n", ivs[start], ivs[i-1], ivs[i]))
			//t.Logf("ids[%d:%d]=%+v", start, i, ids[start:i])
			start = i
			prv = ivs[i]
			b.WriteString(indent)
		} else {
			prv++
		}
	}
	b.WriteString(fmt.Sprintf("%d->%d\n", ivs[start], ivs[len(ids)-1]))
}

func loopCheckRanges(cdi checkDiffInfo, rs []Range) (tblSum tblCheckSummary, ids IDsT) {
	LogInfo("loopCheckRanges rs=%+v", rs)
	pk, err := findPrimaryKey(cdi.pg, cdi.tbl)
	if err != nil {
		if err == ErrNoPrimaryKey {
			tblSum.desc = fmt.Sprintf("'%s.%s' was no primary key.not support", cdi.pg.dbname, cdi.tbl)
			LogErr(tblSum.desc)
			return tblSum, nil
		}
		tblSum.desc = fmt.Sprintf("'%s.%s' findPrimaryKey err:%s", cdi.pg.dbname, cdi.tbl, err.Error())
		LogErr(tblSum.desc)
		return tblSum, nil
	}
	var old []Range
	for {
		news, r2, err := reCheckRanges(cdi, rs, pk)
		old = append(old, r2...)
		if err != nil {
			tblSum.desc = err.Error()
			tblSum.rs = append(tblSum.rs, old...)
			tblSum.rs = append(tblSum.rs, news...)
			return
		}
		if len(news) == 0 {
			tblSum.rs = append(tblSum.rs, old...)
			return
		}
		rs = news
		if rs[0].count < 100 {
			tblSum.rs = append(tblSum.rs, old...)
			break
		}
	}
	for _, r := range rs {
		ret, err := crcRange(cdi, r, pk)
		if err != nil {
			tblSum.rs = append(tblSum.rs, r)
			continue
		}
		ids = append(ids, ret...)
	}

	return
}

type IdT string

type IDsT []IdT

// func (ids IDsT) isNumber() bool {
// 	if 0 == ids.checkNumber {
// 		_, e := strconv.ParseInt(string(ids.ids[0]), 10, 64)
// 		if e == nil {
// 			ids.checkNumber = 1
// 		} else {
// 			ids.checkNumber = -1
// 		}
// 	}
// 	return ids.checkNumber == 1
// }
//
// func (ids IDsT) Len() int {
// 	return len(ids.ids)
// }
//
// func (ids IDsT) Less(i, j int) bool {
// 	if ids.isNumber() {
// 		if len(ids.ids[i]) < len(ids.ids[j]) {
// 			return true
// 		}
// 		if len(ids.ids[i]) > len(ids.ids[j]) {
// 			return false
// 		}
// 		return ids.ids[i] < ids.ids[j]
// 	}
// 	return ids.ids[i] < ids.ids[j]
// }
//
// func (ids IDsT) Swap(i, j int) {
// 	ids.ids[i], ids.ids[j] = ids.ids[j], ids.ids[i]
// }

func isNumber(id string) bool {
	//is number?
	_, e := strconv.ParseInt(id, 10, 64)
	return e == nil
}

func reCheckRanges(cdi checkDiffInfo, rs []Range, pk string) (news, old []Range, err error) {
	//stop 条件;
	if len(rs) == 0 {
		return
	}

	for _, r := range rs {
		tblSum1 := reCheckRange(cdi, r, pk)
		if tblSum1.desc != "" { //tbl error,return .
			err = fmt.Errorf(tblSum1.desc)
			return
		}
		// if len(tblSum1.rs) > cdi.parall/4 {
		// 	old = append(old, r)
		// } else {
		news = append(news, tblSum1.rs...)
		//}
	}
	return
}

func crcRange(info checkDiffInfo, r Range, pk string) (ids IDsT, err error) {
	db1, db2 := info.pg, info.tidb
	// parall := info.parall
	// if parall > len(info.rs) {
	// 	parall = len(info.rs)
	// }
	parall := 1
	LogInfo("crcRange(%s.%s;r=%+v;%v->%v;parallel=%d) staring...\n", db1.dbname, info.tbl, r, db1.dt, db2.dt, parall)
	//get xor;
	crc1, err1 := crc32(db1, info.tbl)
	if err1 != nil {
		LogErr("\t crcRange %v->%v; ", db1.dt, err1)
		return nil, err1
	}
	var kv1 = map[string]string{}
	var id string
	var str string
	query := fmt.Sprintf("select id,%s from %s where %s>=%s and %s<=%s", crc1, info.tbl, pk, r.start, pk, r.end)
	rows, err := db1.db.Query(query)
	if err != nil {
		LogErr("\t crcRange.query1 %v->%v; ", db1.dt, err)
		return nil, err
	}
	for rows.Next() {
		err = rows.Scan(&id, &str)
		if err != nil {
			LogErr("\t crcRange.scan1 %v->%v; ", db1.dt, err)
			return nil, err
		}
		kv1[id] = str
	}
	rows, err = db2.db.Query(query)
	if err != nil {
		LogErr("\t crcRange.query2 %v->%v; ", db1.dt, err)
		return nil, err
	}
	var rets IDsT
	for rows.Next() {
		err = rows.Scan(&id, &str)
		if err != nil {
			LogErr("\t crcRange.scan2 %v->%v; ", db1.dt, err)
			return nil, err
		}
		crc := kv1[id]
		if crc != str {
			rets = append(rets, IdT(id))
		}
		delete(kv1, id)
	}
	if len(kv1) > 0 {
		for k, _ := range kv1 {
			rets = append(rets, IdT(k))
		}
	}
	//LogInfo("crcRange(%s.%s;r=%+v;%v->%v;parallel=%d) finish...:%v\n", db1.dbname, info.tbl, r, db1.dt, db2.dt, parall,rets)
	return rets, nil
}

func reCheckRange(cdi checkDiffInfo, r Range, pk string) tblCheckSummary {
	var tblSum tblCheckSummary
	db := cdi.pg.dbname
	tbl := cdi.tbl
	nthread := cdi.parall
	if nthread > r.count {
		nthread = r.count
	}
	limit := r.count / nthread
	var err error
	cdi.rs, err = SplitChunkRange(cdi.pg.db, tbl, limit, pk, r)
	if err != nil {
		tblSum.desc = fmt.Sprintf("%s.%s SplitChunk err:%s", db, tbl, err.Error())
		LogErr(tblSum.desc)
		return tblSum
	}
	errs, skipTable := CheckDiffByRangesWithNotSkip(cdi, true)
	tblSum.total = len(cdi.rs)
	if skipTable {
		tblSum.desc = fmt.Sprintf("'%s.%s' checkDiffDB check too many erros.skip it and do check it latter.", db, tbl)
		return tblSum
	} else {
		if len(errs) > 0 {
			//tblS.desc = fmt.Sprintf("'%s.%s' checkDiffDB check erros %d:\n", db, tbl, len(errs))
			for _, ei := range errs {
				tblSum.rs = append(tblSum.rs, cdi.rs[ei])
			}
		}
		return tblSum
	}
}
