// coin.go
package coin

import (
	"bytes"
	"container/list"
	"crypto/md5"
	"crypto/rand"
	"encoding/base64"

	"database/sql"
	"encoding/binary"
	"encoding/hex"
	"strconv"

	"io"
	"log"
	"math"
	"math/big"
	"net"

	"fmt"
	"pool/internal/db"
	"pool/internal/msg"
	"pool/internal/server/diff"
	"pool/internal/utility"
	"sync"
	"time"

	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"

	"github.com/rs/zerolog"

	"github.com/redis/go-redis/v9"
	"github.com/zeromq/goczmq"
	"go.uber.org/zap"
	"gopkg.in/natefinch/lumberjack.v2"

	_ "github.com/mattn/go-sqlite3"
)

const JOB_EXPIRED_TIME uint32 = 60         //job expired time (second)
const LOCAL_JOBS_EXPIRED_TIME uint32 = 300 //300 //local jobs expired time (second)
const LOCAL_JOBS_TOTAL_SIZE uint32 = 300   //300   //total local jobs
const CONN_EXPIRED_TIME uint32 = 600       //connection expired time

// miner status
// const MINER_STATUS_OFFLINE string = "offline"
const MINER_STATUS_CONNECTED string = "connected"
const MINER_STATUS_SUBSCRIBED string = "subscribed"
const MINER_STATUS_AUTHORIZED string = "authorized"
const MINER_STATUS_RUNNING string = "online"
const MINER_STATUS_DISCONNECTED string = "offline"
const MINER_STATUS_DISABLED string = "disabled"

const MINER_DURATION_TIME time.Duration = 1
const MINER_DIFFICULTY_ADJUST_DURATION time.Duration = 5

// vardiff
const UP_DIFF int = 0
const DOWN_DIFF int = 1
const UPDATE_DIFF int = 2
const DIFFICULTY_WAIT_TIMES int = 6

const (
	Low  = 0
	Mid  = 1
	Hign = 2
)

type BindConfig struct {
	Listen string `json:"listen"`
	Auth   bool   `json:"auth"`
}

type DiffConfig struct {
	StartDifficulty      float64 `json:"start_diff"`
	DiffMin              float64 `json:"diff_min"`
	DiffMax              float64 `json:"diff_max"`
	DiffAdjustInterval   float64 `json:"diff_adjust_interval"`
	DiffAdjustPercentage float64 `json:"diff_adjust_percentage"`
	DiffAdjustTime       float64 `json:"diff_adjust_time"`
	Filter               string  `json:"filter"`
	Dbg                  bool    `json:"dbg"`
}

type ServerConfig struct {
	Coin     string                  `json:"coin"`
	Host     BindConfig              `json:"host"`
	Diff     DiffConfig              `json:"diff"`
	Zmq      utility.ZmqConfig       `json:"zmq"`
	Redis    utility.RedisConfig     `json:"redis"`
	Zaplog   zap.Config              `json:"zap"`
	Logrotae utility.LogRotateConfig `json:"logrotate"`
}

type CoinObj struct {
	Coin                 string
	Init                 func(server *ServerContext)
	Start                func()
	Stop                 func()
	InitMiner            func(miner *MinerObj)
	HandleMinerSubscribe func(miner *MinerObj, id float64, extranonce1 string, msg string)
	HandleMinerAuth      func(miner *MinerObj)
	HandleMinerSubmit    func(miner *MinerObj, id float64, miner_user string, job_id string, nonce2 string, ntime string, nonce string) (bool, bool, bool)
	SetDifficulty        func(miner *MinerObj)
	Notify               func(miner *MinerObj)
	HandleJobMsg         func(server *ServerContext, Msg []byte)
	IsMhsLow             func(miner *MinerObj) bool
	GetBlockInterval     func() int
}

type PoolBlkMsg struct {
	Height     int64
	Hash       string
	Pow        string
	Net_target string
	Submit     string
	Success    bool
	Accepts    float64
	Rejects    float64
	Reward     float64
	Fee        float64
	Nonce      string
	SubIdx     int64
}

type ServerContext struct {
	CoinCtx CoinObj
	DbCtx   *db.DbContext

	Config *ServerConfig

	PubCh *goczmq.Sock
	SubCh *goczmq.Sock

	Listener net.Listener

	MinerType string

	Extranonce1 uint64

	Difficulty float64

	RefDifficulty float64

	Accepts         float64
	Rejects         float64
	AverageHashrate float64

	Miners sync.Map

	MMhs sync.Map

	Started bool

	SLock    sync.Mutex
	ExitFlag bool

	//AlivingChan    chan bool
	//LiveingExpired bool
	FlagAliving     int32
	FlagAlivingExit int32

	ExitPingChan chan bool
	ExitJobChan  chan bool

	Logg *zap.Logger
	LogR *lumberjack.Logger

	SJob msg.StratumJob

	//UpdateMap sync.Map

	SyncJobChan chan bool
	Synced      bool

	ExitDbMiners      chan bool
	ExitDbMinersStats chan bool

	ExitDbUser      chan bool
	ExitDbUserStats chan bool

	ExitDbPoolStats chan bool

	NexaJob msg.NexaStratumJob
	AlphJob msg.AlphStratumJob

	ExitDiffVar chan bool

	RedisClient *redis.Client

	Accepts5M  float64
	Accepts15M float64
	Accepts30M float64
	Accepts1h  float64
	Accepts3h  float64
	Accepts6h  float64
	Accepts12h float64
	Accepts24h float64
	Accepts48h float64

	Rejects5M  float64
	Rejects15M float64
	Rejects30M float64
	Rejects1h  float64
	Rejects3h  float64
	Rejects6h  float64
	Rejects12h float64
	Rejects24h float64
	Rejects48h float64

	Mhs5M  float64
	Mhs15M float64
	Mhs30M float64
	Mhs1h  float64
	Mhs3h  float64
	Mhs6h  float64
	Mhs12h float64
	Mhs24h float64
	Mhs48h float64

	RejectRatio5M  float64
	RejectRatio15M float64
	RejectRatio30M float64
	RejectRatio1h  float64
	RejectRatio3h  float64
	RejectRatio6h  float64
	RejectRatio12h float64
	RejectRatio24h float64
	RejectRatio48h float64

	TotalMiners int64
	Normal      int64
	Abnormal    int64
	Offline     int64
	MhsZero     int64
	MhsLow      int64
	HighRejects int64
	Unstable    int64

	NetTarget string
	NetHight  uint64

	Submits int64
	Blocks  int64
	Orphans int64

	Reward float64
	Fee    float64

	/*Users      sync.Map
	UsersSLock sync.Mutex*/

	/*PoolSLock sync.Mutex*/

	SubIdx int64

	MinerIndex int64

	//NotifyBlkDetailIdx  int32
}

type JobListEntry struct {
	Job_id string
	Ts     time.Time
}

type MhsItem struct {
	Tt   time.Time
	Diff float64
}

type MhsObj struct {
	MinerId         string
	Name            string
	Accepts         []MhsItem
	Rejects         []MhsItem
	StartSubmitTime time.Time

	StartDayTime time.Time

	User   string
	Miner  string
	Index  string
	Status string
	Algo   int
}

type VarDiffOptions struct {
	VariancePercent float64
	AdjustTime      float64
	AdjustInterval  float64
	MinDiff         float64
	MaxDiff         float64
	MinShares       float64
	MaxShares       float64
	TargetShares    float64
	SubmitShares    float64
	SilenceCount    float64
	LastCalcTime    time.Time
	Uptimes         int
	Downtimes       int
	Level           int
	LastSubmitTime  time.Time
}

type BlockMsg struct {
	Target        string
	Submit_target string
	Height        int64
	Success       bool
	Pow           string
	Net_target    string
	Submit        string
	Hash          string
	Header        string
	Accepts       float64
	Total_accepts float64
	Rejects       float64
	Total_rejects float64
	Reward        float64
	Fee           float64
	Nonce         string
	SubIdx        int64
}

type MinerObj struct {
	Server     *ServerContext
	Conn       net.Conn
	Authorized bool

	MinerId string
	JobId   uint32
	Name    string

	Jobs        sync.Map
	LockForJobs sync.Mutex
	JobList     *list.List

	Target           *big.Int
	Difficulty       float64
	DifficultyNext   float64
	ServerDifficulty float64
	ServerTarget     *big.Int
	ServerTargetS    string
	Accepts          float64
	Rejects          float64
	StartSubmitTime  time.Time
	LastSubmitime    time.Time
	SubmitIndex      uint32
	AverageHashrate  float64
	M5Accepts        float64
	M5Hashrate       float64
	M5SubmitTime     time.Time
	LastJobId        string
	LastNonce        string
	CurHeight        uint32
	CurHeight64      uint64
	Reconnect        bool

	LastHeader string

	VarDiffOpt VarDiffOptions

	Version int32
	Job     msg.StratumJob

	User     string
	PassWord string
	Miner    string

	Duration float64

	Status        string
	ConnSetupTime time.Time
	TxLock        sync.Mutex

	KeepliveCnt   float64
	RecvedLiveAck bool
	PongFailCnt   int
	PingCnt       int

	NexaJob msg.NexaStratumJob
	AlphJob msg.AlphStratumJob

	//EndCh chan bool
	FromIP string

	OnlineTime   time.Time
	OfflineTime  time.Time
	Retry        int64
	DurationTime float64
	MinerIndex   int64

	Protocol string

	ErrStaleds    int64
	ErrLowDiffs   int64
	ErrDuplicates int64
	ErrFormats    int64
	ErrOthers     int64

	IsDisabled bool

	Submits int64
	Blocks  int64
	Orphans int64

	Accepts5M  float64
	Accepts15M float64
	Accepts30M float64
	Accepts1h  float64
	Accepts3h  float64
	Accepts6h  float64
	Accepts12h float64
	Accepts24h float64
	Accepts48h float64

	Rejects5M  float64
	Rejects15M float64
	Rejects30M float64
	Rejects1h  float64
	Rejects3h  float64
	Rejects6h  float64
	Rejects12h float64
	Rejects24h float64
	Rejects48h float64

	Mhs5M  float64
	Mhs15M float64
	Mhs30M float64
	Mhs1h  float64
	Mhs3h  float64
	Mhs6h  float64
	Mhs12h float64
	Mhs24h float64
	Mhs48h float64

	RejectRatio5M  float64
	RejectRatio15M float64
	RejectRatio30M float64
	RejectRatio1h  float64
	RejectRatio3h  float64
	RejectRatio6h  float64
	RejectRatio12h float64
	RejectRatio24h float64
	RejectRatio48h float64

	Reward float64
	Fee    float64

	Zlog     zerolog.Logger
	LogR     *lumberjack.Logger
	ZlogInit bool

	//EndCh chan bool

	DiffHandler diff.KalmanVarDiff

	NeedExit int32
}

/*type UserBlockMsg struct {
	User       string
	Miner      string
	Index      string
	Height     int64
	Hash       string
	Pow        string
	Net_target string
	Submit     string
	Success    bool
	Accepts    float64
	Rejects    float64
	Reward     float64
	Fee        float64
	Nonce      string
	SubIdx     int64
}*/
/*
type UserMinerContainer struct {
	Data map[string]string
}*/

/*
type UserObj struct {
	Server *ServerContext
	User   string
	Name   string

	Normal   int64
	Abnormal int64

	Offline     int64
	MhsZero     int64
	MhsLow      int64
	HighRejects int64
	Unstable    int64

	Submits int64
	Blocks  int64
	Orphans int64

	Reward float64
	Fee    float64

	Accepts5M  float64
	Accepts15M float64
	Accepts30M float64
	Accepts1h  float64
	Accepts3h  float64
	Accepts6h  float64
	Accepts12h float64
	Accepts24h float64
	Accepts48h float64

	Rejects5M  float64
	Rejects15M float64
	Rejects30M float64
	Rejects1h  float64
	Rejects3h  float64
	Rejects6h  float64
	Rejects12h float64
	Rejects24h float64
	Rejects48h float64

	Mhs5M  float64
	Mhs15M float64
	Mhs30M float64
	Mhs1h  float64
	Mhs3h  float64
	Mhs6h  float64
	Mhs12h float64
	Mhs24h float64
	Mhs48h float64

	RejectRatio5M  float64
	RejectRatio15M float64
	RejectRatio30M float64
	RejectRatio1h  float64
	RejectRatio3h  float64
	RejectRatio6h  float64
	RejectRatio12h float64
	RejectRatio24h float64
	RejectRatio48h float64
}*/

func md5md5(v string) string {
	md5Obj := md5.New()
	md5Obj.Write([]byte(v))
	char := md5Obj.Sum(nil)
	return hex.EncodeToString(char)
}

func Guid() string {
	c := make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, c); err != nil {
		return ""
	}
	return md5md5(base64.URLEncoding.EncodeToString(c))
}

func VarAdjustDifficulty(miner *MinerObj, adjust int) {
	if adjust != UP_DIFF && adjust != DOWN_DIFF && adjust != UPDATE_DIFF {
		miner.Server.Logg.Error("[server]", zap.Int("Not support adjust ", adjust))
		return
	}
	if adjust == UP_DIFF {
		miner.DifficultyNext = miner.Difficulty
		if miner.VarDiffOpt.Level == Mid {
			miner.DifficultyNext *= math.Pow(2, 1)
			miner.DifficultyNext = math.Round(miner.DifficultyNext*1000) / 1000
		} else if miner.VarDiffOpt.Level == Hign {
			miner.DifficultyNext *= math.Pow(2, 2)
			miner.DifficultyNext = math.Round(miner.DifficultyNext*1000) / 1000
		}
	} else if adjust == DOWN_DIFF {
		miner.DifficultyNext = miner.Difficulty
		if miner.VarDiffOpt.Level == Mid {
			miner.DifficultyNext /= math.Pow(2, 1)
			miner.DifficultyNext = math.Round(miner.DifficultyNext*1000) / 1000
		} else if miner.VarDiffOpt.Level == Hign {
			miner.DifficultyNext /= math.Pow(2, 2)
			miner.DifficultyNext = math.Round(miner.DifficultyNext*1000) / 1000
		}
	} else if adjust == UPDATE_DIFF {
		if miner.VarDiffOpt.SubmitShares > 0 {
			// re-target if outside bounds
			if miner.VarDiffOpt.SubmitShares < miner.VarDiffOpt.MinShares || miner.VarDiffOpt.SubmitShares > miner.VarDiffOpt.MaxShares {
				var change float64 = miner.VarDiffOpt.SubmitShares / miner.VarDiffOpt.TargetShares
				miner.DifficultyNext = miner.Difficulty
				miner.DifficultyNext *= change
				miner.DifficultyNext = math.Round(miner.DifficultyNext*1000) / 1000
			}
			miner.VarDiffOpt.SilenceCount = 0
		} else {
			// radical measures if there were no shares submitted
			miner.VarDiffOpt.SilenceCount++
			miner.DifficultyNext = miner.Difficulty / math.Pow(2, miner.VarDiffOpt.SilenceCount)
			miner.DifficultyNext = math.Round(miner.DifficultyNext*1000) / 1000
		}
	}

	if miner.DifficultyNext < miner.VarDiffOpt.MinDiff {
		miner.DifficultyNext = miner.VarDiffOpt.MinDiff
	} else if miner.DifficultyNext > miner.VarDiffOpt.MaxDiff {
		miner.DifficultyNext = miner.VarDiffOpt.MaxDiff
	}
	miner.VarDiffOpt.TargetShares = miner.VarDiffOpt.AdjustTime / miner.VarDiffOpt.AdjustInterval * miner.DifficultyNext
	miner.VarDiffOpt.MinShares = miner.VarDiffOpt.AdjustTime / miner.VarDiffOpt.AdjustInterval * miner.DifficultyNext * (1 - miner.VarDiffOpt.VariancePercent)
	miner.VarDiffOpt.MaxShares = miner.VarDiffOpt.AdjustTime / miner.VarDiffOpt.AdjustInterval * miner.DifficultyNext * (1 + miner.VarDiffOpt.VariancePercent)
	miner.VarDiffOpt.SubmitShares = 0
	miner.VarDiffOpt.Uptimes = 0
	miner.VarDiffOpt.Downtimes = 0
	miner.Server.Logg.Info("[server]", zap.Float64("DifficultyNext", miner.DifficultyNext))
	miner.Server.Logg.Info("[server]", zap.Float64("TargetShares", miner.VarDiffOpt.TargetShares), zap.Float64("MinShares", miner.VarDiffOpt.MinShares), zap.Float64("MaxShares", miner.VarDiffOpt.MaxShares))

	now := time.Now()
	share_interval := now.Sub(miner.LastSubmitime).Seconds()
	New_diff_into_db(miner.User, miner.Miner, fmt.Sprint(miner.MinerIndex), miner.Difficulty, miner.DifficultyNext, miner.VarDiffOpt.SubmitShares, share_interval, miner.VarDiffOpt.MinShares, miner.VarDiffOpt.MaxShares)
}

var gdiff_db *sql.DB

func Init_diff_db() {
	db, err := sql.Open("sqlite3", "./diffs.db")
	if err != nil {
		log.Printf("Error opening database: %v", err)
		return
	}
	//defer db.Close()

	gdiff_db = db

	createTableSQL := `
					  CREATE TABLE IF NOT EXISTS diffs (
					  	id INTEGER PRIMARY KEY AUTOINCREMENT,
						ts TEXT NOT NULL,
						user TEXT NOT NULL,
						miner TEXT NOT NULL,
						minerid TEXT NOT NULL,
					  	diff REAL NOT NULL,
						next REAL NOT NULL,
						kp  REAL NOT NULL,
						interval  REAL NOT NULL,
						mhs  REAL NOT NULL,
						mhs_est  REAL NOT NULL
					  );`
	_, err = gdiff_db.Exec(createTableSQL)
	if err != nil {
		log.Printf("Error creating table: %v", err)
		return
	}
}

func New_diff_into_db(user string, miner string, minerid string, diff float64, diff_next float64, kp float64, interval float64, mhs float64, mhs_est float64) {
	if gdiff_db != nil {
		insertSQL := `INSERT INTO diffs (ts, user, miner, minerid, diff, next, kp, interval, mhs, mhs_est) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
		_, err := gdiff_db.Exec(insertSQL, time.Now().Format("2006-01-02 15:04:05"), user, miner, minerid, diff, diff_next, kp, interval, mhs, mhs_est)
		if err != nil {
			log.Printf("Error inserting data from diffs %s: %v", user+"."+miner+"_"+minerid, err)
			return
		}
	}
}

func DiffStop() {
	if gdiff_db != nil {
		defer gdiff_db.Close()
	}
}

func Build_coinbase(coinbase1 string, extranonce1 string, extranonce2, coinbase2 string, doublehash bool) ([]byte, error) {
	var coinbase_string string = coinbase1 + extranonce1 + extranonce2 + coinbase2
	coinbase_bytes, err := hex.DecodeString(coinbase_string)
	if err != nil {
		//log.Println("[dash]failed to Build_coinbase", err)
		return nil, err
	}
	//fmt.Println("coinbase", coinbase_string, hex.EncodeToString(coinbase_bytes))
	var coinbase []byte
	if doublehash {
		coinbase = chainhash.DoubleHashB(coinbase_bytes)
	} else {
		coinbase = chainhash.HashB(coinbase_bytes)
	}
	//fmt.Println("coinbase_hash", hex.EncodeToString(coinbase))
	//fmt.Println("coinbase_hash", coinbase)
	return coinbase, nil
}

// build merkel root hash value
func Build_merkle_root(coinbase []byte, transaction_list *[]string) ([]byte, error) {
	var merkle_root []byte
	merkle_root = coinbase
	count := len(*transaction_list)
	var i int
	for i = 0; i < count; i++ {
		//fmt.Println("merkle_hash start", i, hex.EncodeToString(merkle_root))
		t_bytes, err := hex.DecodeString((*transaction_list)[i])
		if err != nil {
			//log.Println("[dash]failed to Build_merkle_root", err)
			return nil, err
		}
		for j := 0; j < len(t_bytes); j++ {
			merkle_root = append(merkle_root, t_bytes[j])
		}
		//fmt.Println("merkle_hash append", i, hex.EncodeToString(merkle_root))
		merkle := chainhash.DoubleHashB(merkle_root)
		//fmt.Println("merkle_hash", i, hex.EncodeToString(merkle))
		merkle_root = merkle
	}
	return merkle_root, nil
}

func BuildBlockHash(job *msg.StratumJob, doublehash bool, pow func(h wire.BlockHeader) chainhash.Hash) ([]byte, wire.BlockHeader) {
	var bh wire.BlockHeader
	//log.Printf("[dash]c1 %s,e1 %s,e2 %s,c2 %s\n", job.Coinbase1, job.Extranonce1, job.Extranonce2, job.Coinbase2)
	coinbase, err := Build_coinbase(job.Coinbase1, job.Extranonce1, job.Extranonce2, job.Coinbase2, doublehash)
	if err != nil {
		return nil, bh
	}
	//log.Println("[dash]trans", job.Transactions)
	merkle_root, err := Build_merkle_root(coinbase, job.Transactions)
	if err != nil {
		return nil, bh
	}
	vb := make([]byte, 4)
	binary.LittleEndian.PutUint32(vb, uint32(job.Version))
	tb := make([]byte, 4)
	binary.LittleEndian.PutUint32(tb, uint32(job.Timestamp.Unix()))
	bh, err = Build_block_hash(hex.EncodeToString(vb), job.PrevblockBig, merkle_root, hex.EncodeToString(tb), job.BitsS, job.Nonce)
	if err != nil {
		return nil, bh
	}
	powhash := pow(bh)
	//log.Printf("POW hash:%s", powhash.String())
	return powhash.CloneBytes(), bh
}

func Build_block_hash(block_version string, prev_hash string, merkle_root []byte, ntime string, nbits string, nonce string) (wire.BlockHeader, error) {
	var header wire.BlockHeader
	b, err := strconv.ParseUint(nbits, 16, 32)
	if err != nil {
		//log.Println("[dash]failed to Build_block_hash", err)
		return header, err
	}
	header.Bits = uint32(b)
	//log.Printf("Bits:%x", header.Bits)
	p_bytes, err := hex.DecodeString(prev_hash)
	if err != nil {
		log.Println("[dash]failed to Build_block_hash", err)
		return header, err
	}
	p, err := chainhash.NewHash(utility.Convert_big_endian(p_bytes))
	header.PrevBlock = *p
	//log.Printf("Prev Block:%s", hex.EncodeToString(header.PrevBlock.CloneBytes()))
	m, err := chainhash.NewHash(merkle_root)
	header.MerkleRoot = *m
	//log.Printf("MerkleRoot:%s", hex.EncodeToString(header.MerkleRoot.CloneBytes()))
	n, err := strconv.ParseUint(nonce, 16, 32)
	if err != nil {
		//log.Println("[dash]failed to Build_block_hash", err)
		return header, err
	}
	header.Nonce = uint32(n)
	//log.Printf("Nonce:%x", header.Nonce)
	v, err := hex.DecodeString(block_version)
	if err != nil {
		//log.Println("[dash]failed to Build_block_hash", err)
		return header, err
	}
	vBuffer := bytes.NewBuffer(v)
	binary.Read(vBuffer, binary.BigEndian, &(header.Version))
	//log.Printf("Version:%x", header.Version)
	t, err := strconv.ParseInt(ntime, 16, 64)
	if err != nil {
		//log.Println("[dash]failed to Build_block_hash", err)
		return header, err
	}
	header.Timestamp = time.Unix(t, 0)
	//log.Printf("nTime %v", header.Timestamp)

	return header, nil
}
