package htpasswd

import (
	"bytes"
	"crypto/md5"
	"fmt"
	"strings"
)

type md5Password struct {
	salt   string
	hashed string
	prefix string
}

// PrefixCryptMd5 is the Md5crypt hash prefix
const PrefixCryptMd5 = "$1$"

// PrefixCryptApr1 is the Apache Apr1 hash prefix
const PrefixCryptApr1 = "$apr1$"

// AcceptMd5 accepts valid MD5 encoded passwords
func AcceptMd5(src string) (EncodedPasswd, error) {
	if !strings.HasPrefix(src, PrefixCryptApr1) && !strings.HasPrefix(src, PrefixCryptMd5) {
		return nil, nil
	}

	prefix := PrefixCryptApr1
	if strings.HasPrefix(src, PrefixCryptMd5) {
		prefix = PrefixCryptMd5
	}
	rest := strings.TrimPrefix(src, prefix)
	mparts := strings.SplitN(rest, "$", 2)
	if len(mparts) != 2 {
		return nil, fmt.Errorf("malformed md5 password: %s", src)
	}

	salt, hashed := mparts[0], mparts[1]
	return &md5Password{salt, hashed, prefix}, nil
}

// RejectMd5 rejects any MD5 encoded password
func RejectMd5(src string) (EncodedPasswd, error) {
	if !strings.HasPrefix(src, PrefixCryptApr1) && !strings.HasPrefix(src, PrefixCryptMd5) {
		return nil, nil
	}
	return nil, fmt.Errorf("md5 password rejected: %s", src)
}

// This is the MD5 hashing function out of Apache's htpasswd program. The algorithm
// is insane, but we have to match it. Mercifully I found a PHP variant of it at
//
//	http://stackoverflow.com/questions/2994637/how-to-edit-htpasswd-using-php
//
// in an answer. That reads better than the original C, and is easy to instrument.
// We will eventually go back to the original apr_md5.c for inspiration when the
// PHP gets too weird.
// The algorithm makes more sense if you imagine the original authors in a pub,
// drinking beer and rolling dice as the fundamental design process.
// Note that this is the same algorithm used in md5Crypt except for the prefix in md5Crypt is $1$
// while in apr1 it's $apr1$
func md5Crypt(password string, salt string, prefix string) string {

	// start with a hash of password and salt
	initBin := md5.Sum([]byte(password + salt + password))

	// begin an initial string with hash and salt
	initText := bytes.NewBufferString(password + prefix + salt)

	// add crap to the string willy-nilly
	for i := len(password); i > 0; i -= 16 {
		lim := i
		if lim > 16 {
			lim = 16
		}
		initText.Write(initBin[0:lim])
	}

	// add more crap to the string willy-nilly
	for i := len(password); i > 0; i >>= 1 {
		if (i & 1) == 1 {
			initText.WriteByte(byte(0))
		} else {
			initText.WriteByte(password[0])
		}
	}

	// Begin our hashing in earnest using our initial string
	bin := md5.Sum(initText.Bytes())

	n := bytes.NewBuffer([]byte{})

	for i := 0; i < 1000; i++ {
		// prepare to make a new muddle
		n.Reset()

		// alternate password+crap+bin with bin+crap+password
		if (i & 1) == 1 {
			n.WriteString(password)
		} else {
			n.Write(bin[:])
		}

		// usually add the salt, but not always
		if i%3 != 0 {
			n.WriteString(salt)
		}

		// usually add the password but not always
		if i%7 != 0 {
			n.WriteString(password)
		}

		// the back half of that alternation
		if (i & 1) == 1 {
			n.Write(bin[:])
		} else {
			n.WriteString(password)
		}

		// replace bin with the md5 of this muddle
		bin = md5.Sum(n.Bytes())
	}

	// At this point we stop transliterating the PHP code and flip back to
	// reading the Apache source. The PHP uses their base64 library, but that
	// uses the wrong character set so needs to be repaired afterwards and reversed
	// and it is just really weird to read.

	result := bytes.NewBuffer([]byte{})

	// This is our own little similar-to-base64-but-not-quite filler
	fill := func(a byte, b byte, c byte) {
		v := (uint(a) << 16) + (uint(b) << 8) + uint(c) // take our 24 input bits

		for i := 0; i < 4; i++ { // and pump out a character for each 6 bits
			result.WriteByte("./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[v&0x3f])
			v >>= 6
		}
	}

	// The order of these indices is strange, be careful
	fill(bin[0], bin[6], bin[12])
	fill(bin[1], bin[7], bin[13])
	fill(bin[2], bin[8], bin[14])
	fill(bin[3], bin[9], bin[15])
	fill(bin[4], bin[10], bin[5]) // 5?  Yes.
	fill(0, 0, bin[11])

	resultString := string(result.Bytes()[0:22]) // we wrote two extras since we only need 22.

	return resultString
}

func (m *md5Password) MatchesPassword(pw string) bool {
	hashed := md5Crypt(pw, m.salt, m.prefix)
	return constantTimeEquals(hashed, m.hashed)
}
