package phphelper

import (
	"crypto/md5"
	"fmt"
	str "github.com/fthvgb1/wp-go/helper/strings"
	"golang.org/x/crypto/bcrypt"
	"io"
	"os"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"
)

// PasswordHash 目前还不完善，只有 Encode64 getRandomBytes CryptPrivate 方法能用
type PasswordHash struct {
	itoa64             string
	iterationCountLog2 int
	portableHashes     bool
	randomState        string
}

func NewPasswordHash(iterationCountLog2 int, portableHashes bool) *PasswordHash {
	if iterationCountLog2 < 4 || iterationCountLog2 > 31 {
		iterationCountLog2 = 8
	}
	return &PasswordHash{
		iterationCountLog2: iterationCountLog2,
		portableHashes:     portableHashes,
		itoa64:             "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
		randomState:        strconv.Itoa(os.Getgid()),
	}
}

func (p *PasswordHash) getRandomBytes(count int) (r string, err error) {
	urand := "/dev/urandom"
	f, err := os.OpenFile(urand, os.O_RDWR, 0644)
	if err != nil {
		return "", err
	}
	defer f.Close()
	buf := make([]byte, count)
	_, err = f.Read(buf)
	if err != nil {
		return "", err
	}
	r = string(buf)
	if len(buf) < count {
		r = ""
		for i := 0; i < count; i = i + 16 {
			p.randomState = str.Md5(fmt.Sprintf("%d%s", time.Now().UnixMilli(), p.randomState))

			n, err := md5Raw(p.randomState)
			if err != nil {
				return "", err
			}
			r = fmt.Sprintf("%s%s", r, n)
		}
		r = r[0:count]
	}
	return
}

func (p *PasswordHash) Encode64(input string, count int) (out string) {
	i := 0
	s := strings.Builder{}
	for i < count {
		v := 0
		if i < len(input) {
			v = int(input[i])
		}
		s.WriteString(string(p.itoa64[v&0x3f]))
		i++
		if i < count {
			if i < len(input) {
				v |= int(input[i]) << 8
			} else {
				v = 0
			}
		}
		s.WriteString(string(p.itoa64[(v>>6)&0x3f]))
		if i >= count {
			break
		}
		i++
		if i < count {
			if i < len(input) {
				v |= int(input[i]) << 16
			} else {
				v = 0
			}
		}

		s.WriteString(string(p.itoa64[(v>>12)&0x3f]))
		if i >= count {
			break
		}
		i++
		s.WriteString(string(p.itoa64[(v>>18)&0x3f]))
	}
	out = s.String()
	return
}

func (p *PasswordHash) CryptPrivate(password, set string) (rr string, err error) {
	rr = "*0"
	r := []rune(rr)
	setting := []rune(set)
	if string(r) == string(setting[0:2]) {
		rr = "*1"
	}
	id := setting[0:3]
	idx := string(id)
	if idx != "$P$" && idx != "$H$" {
		return
	}
	log2 := strings.Index(p.itoa64, string(setting[3]))
	if log2 < 7 || log2 > 30 {
		return
	}
	count := 1 << log2
	l := 12
	if len(setting) < 12 {
		l = len(setting)
	}
	salt := setting[4:l]
	if len(salt) != 8 {
		return
	}
	hash, err := md5Raw(fmt.Sprintf("%s%s", string(salt), password))
	if err != nil {
		return
	}
	for i := 0; i < count; i++ {
		hash, err = md5Raw(fmt.Sprintf("%s%s", hash, password))
		if err != nil {
			return
		}
	}
	rr = string(setting[0:l])
	rr = fmt.Sprintf("%s%s", rr, p.Encode64(hash, 16))
	return
}

func (p *PasswordHash) genSaltBlowFish(input string) (out string, err error) {
	itoa64 := "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
	s := strings.Builder{}
	s.WriteString("$2a$")
	s.WriteString(fmt.Sprintf("%c", '0'+p.iterationCountLog2/10))
	s.WriteString(fmt.Sprintf("%c", '0'+p.iterationCountLog2%10))
	s.WriteString("$")
	i := 0
	for {
		c1 := int(input[i])
		i++
		s.WriteString(string(itoa64[c1>>2]))
		c1 = (c1 & 0x03) << 4
		if i >= 16 {
			s.WriteString(string(itoa64[c1]))
			break
		}

		c2 := int(input[i])
		i++
		c1 |= c2 >> 4
		s.WriteString(string(input[c1]))
		c1 = (c2 & 0x0f) << 2
		c2 = int(input[i])
		i++
		c1 |= c2 >> 6
		s.WriteString(string(itoa64[c1]))
		s.WriteString(string(itoa64[c2]))
	}
	out = s.String()
	return
}

func (p *PasswordHash) HashPassword(pass string) (r string, err error) {
	if utf8.RuneCountInString(pass) > 4096 {
		r = "*"
		return
	}
	random := ""
	hash := ""
	if !p.portableHashes {
		random, err = p.getRandomBytes(16)
		if err != nil {
			return
		}
		h, er := bcrypt.GenerateFromPassword([]byte(pass), 16)
		if er != nil {
			err = er
			return
		}
		hash = string(h)
		if len(hash) == 60 {
			r = hash
			return
		}
	}
	if len(random) < 6 {
		random, err = p.getRandomBytes(6)
		if err != nil {
			return
		}
		salt := p.genSaltPrivate(random)
		hash, err = p.CryptPrivate(pass, salt)
		if err != nil {
			return
		}
		if len(hash) == 34 {
			r = hash
			return
		}
	}
	r = "*"
	return
}

func (p *PasswordHash) CheckPassword(pass, hash string) bool {
	if utf8.RuneCountInString(pass) > 4096 {
		return false
	}
	err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass))
	if err != nil {
		return false
	}
	return true
}

func (p *PasswordHash) genSaltPrivate(input string) string {
	s := strings.Builder{}
	s.WriteString("$P$")
	min := 30
	if p.iterationCountLog2+5 < 30 {
		min = p.iterationCountLog2 + 5
	}
	s.WriteString(string(p.itoa64[min]))
	s.WriteString(p.Encode64(input, 6))
	return s.String()
}

func md5Raw(s string) (string, error) {
	h := md5.New()
	_, err := io.WriteString(h, s)
	if err != nil {
		return "", err
	}
	return string(h.Sum(nil)), err
}
