// Package htpasswd contains utilities for manipulating .htpasswd files
package htpasswd

import (
	"errors"
	"fmt"
	"os"
	"strings"
)

// Passwds name => hash
type Passwds map[string]string

// Hasher interface implemented by hash algos
type Hasher interface {
	Hash(password string) (string, error)
	Match(password, hashedPassword string) error
	Name() string
	Prefix() string
}

// ParseHtpasswdFile parses a .htpasswd file
// and returns a Passwd type
func ParseHtpasswdFile(file string) (Passwds, error) {
	htpasswdBytes, err := os.ReadFile(file)
	if err != nil {
		return nil, err
	}
	return ParseHtpasswd(htpasswdBytes)
}

// ParseHtpasswd parses a slice of bytes in htpasswd style
func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
	lines := strings.Split(string(htpasswdBytes), "\n")
	passwords := make(map[string]string)
	var err error

	for lineNumber, line := range lines {
		line = strings.Trim(line, " ")
		if len(line) == 0 {
			// skipping empty lines
			continue
		}

		parts := strings.Split(line, ":")
		if ok, err := validLine(parts, lineNumber, line); !ok {
			return passwords, err
		}

		parts = trimParts(parts)
		_, exists := passwords[parts[0]]

		if exists {
			err = errors.New("invalid htpasswords file - user " +
				parts[0] + " defined more than once")
			return passwords, err
		}
		passwords[parts[0]] = parts[1]
	}
	return passwords, err
}

// CreateUser creates a record in the named file with
// the named password and hash algorithm
func CreateUser(file, user, passwd string, algo Hasher) error {
	pp, err := ParseHtpasswdFile(file)
	if err != nil {
		return err
	}
	err = pp.CreateUser(user, passwd, algo)
	if err != nil {
		return err
	}
	return pp.Write(file)
}

// CreateUser will create a new user in the given Passwd object
// using the given name, password and hashing algorithm
func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error {
	if _, exists := pp[user]; exists {
		return fmt.Errorf("user %s already exists", user)
	}
	h, err := algo.Hash(passwd)
	if err != nil {
		return err
	}
	pp[user] = h
	return nil
}

// UpdateUser will update the password for the named user
// in the named file
func UpdateUser(file, user, passwd string, algo Hasher) error {
	pp, err := ParseHtpasswdFile(file)
	if err != nil {
		return err
	}
	err = pp.UpdateUser(user, passwd, algo)
	if err != nil {
		return err
	}
	return pp.Write(file)
}

// UpdateUser will update the password for the named user
// using the given name, password and hashing algorithm
func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error {
	if _, exists := pp[user]; !exists {
		return fmt.Errorf("user %s does not exist", user)
	}
	h, err := algo.Hash(passwd)
	if err != nil {
		return err
	}
	pp[user] = h
	return nil
}

// DeleteUser deletes the named user from the named file
func DeleteUser(file, user string) error {
	pp, err := ParseHtpasswdFile(file)
	if err != nil {
		return err
	}
	err = pp.DeleteUser(user)
	if err != nil {
		return err
	}

	return pp.Write(file)
}

// DeleteUser deletes the named user from the named file
func (pp Passwds) DeleteUser(user string) error {
	if _, exists := pp[user]; !exists {
		return fmt.Errorf("user %s does not exist", user)
	}

	delete(pp, user)
	return nil
}

// VerifyUser will check if the given user and password are matching
// with the content of the given file
func VerifyUser(file, user, passwd string) error {
	pp, err := ParseHtpasswdFile(file)
	if err != nil {
		return err
	}
	return pp.VerifyUser(user, passwd)
}

// VerifyUser will check if the given user and password are matching
// with the given Passwd object
func (pp Passwds) VerifyUser(user, passwd string) error {
	if _, ok := pp[user]; !ok {
		return fmt.Errorf("user %s does not exist", user)
	}
	alg, err := identifyHash(pp[user])
	if err != nil {
		return fmt.Errorf("cannot identify algo %v", alg)
	}
	return alg.Match(passwd, pp[user])
}

// Write will cwrite the Passwd object to the given file
func (pp Passwds) Write(file string) error {
	return os.WriteFile(file, pp.Bytes(), os.ModePerm)
}

// Bytes will return the Passwd as a byte slice
func (pp Passwds) Bytes() []byte {
	pass := []byte{}
	for name, hash := range pp {
		pass = append(pass, []byte(name+":"+hash+"\n")...)
	}
	return pass
}

func identifyHash(h string) (Hasher, error) {
	switch {
	case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
		strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
		return new(Bcrypt), nil
	case strings.HasPrefix(h, "$apr1$"):
		return new(Apr1), nil
	case strings.HasPrefix(h, "{SHA}"):
		return new(Sha), nil
	case strings.HasPrefix(h, "{SSHA}"):
		return new(Ssha), nil
	case strings.HasPrefix(h, "$5$"):
		return new(Sha256), nil
	case strings.HasPrefix(h, "$6$"):
		return new(Sha512), nil
	}

	return nil, fmt.Errorf("unsupported hash algorithm")
}

func validLine(parts []string, lineNumber int, line string) (bool, error) {
	var err error
	if len(parts) != 2 {
		err = errors.New(fmt.Sprintln("invalid line", lineNumber+1,
			"unexpected number of parts split by", ":", len(parts),
			"instead of 2 in\"", line, "\""))
		return false, err
	}
	return true, nil
}

func trimParts(parts []string) []string {
	for i, part := range parts {
		parts[i] = strings.Trim(part, " ")
	}
	return parts
}