package passwd

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/rancher/k3s/pkg/token"
	"github.com/rancher/k3s/pkg/util"
)

type entry struct {
	pass string
	role string
}

type Passwd struct {
	changed bool
	names   map[string]entry
}

func Read(file string) (*Passwd, error) {
	result := &Passwd{
		names: map[string]entry{},
	}

	f, err := os.Open(file)
	if err != nil {
		if os.IsNotExist(err) {
			return result, nil
		}
		return nil, err
	}
	defer f.Close()

	reader := csv.NewReader(f)
	reader.FieldsPerRecord = -1
	for {
		record, err := reader.Read()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, err
		}
		if len(record) < 2 {
			return nil, fmt.Errorf("password file '%s' must have at least 2 columns (password, name), found %d", file, len(record))
		}
		e := entry{
			pass: record[0],
		}
		if len(record) > 3 {
			e.role = record[3]
		}
		result.names[record[1]] = e
	}

	return result, nil
}

func (p *Passwd) Check(name, pass string) (matches bool, exists bool) {
	e, ok := p.names[name]
	if !ok {
		return false, false
	}
	return e.pass == pass, true
}

func (p *Passwd) EnsureUser(name, role, passwd string) error {
	tokenPrefix := "::" + name + ":"
	idx := strings.Index(passwd, tokenPrefix)
	if idx > 0 && strings.HasPrefix(passwd, "K10") {
		passwd = passwd[idx+len(tokenPrefix):]
	}

	if e, ok := p.names[name]; ok {
		if passwd != "" && e.pass != passwd {
			p.changed = true
			e.pass = passwd
		}

		if e.role != role {
			p.changed = true
			e.role = role
		}

		p.names[name] = e
		return nil
	}

	if passwd == "" {
		token, err := token.Random(16)
		if err != nil {
			return err
		}
		passwd = token
	}

	p.changed = true
	p.names[name] = entry{
		pass: passwd,
		role: role,
	}

	return nil
}

func (p *Passwd) Users() []string {
	users := []string{}
	for user := range p.names {
		users = append(users, user)
	}
	return users
}

func (p *Passwd) Pass(name string) (string, bool) {
	e, ok := p.names[name]
	if !ok {
		return "", false
	}
	return e.pass, true
}

func (p *Passwd) Write(passwdFile string) error {
	if !p.changed {
		return nil
	}

	var records [][]string
	for name, e := range p.names {
		records = append(records, []string{
			e.pass,
			name,
			name,
			e.role,
		})
	}

	return writePasswords(passwdFile, records)
}

func writePasswords(passwdFile string, records [][]string) error {
	err := func() error {
		// ensure to close tmp file before rename for filesystems like NTFS
		out, err := os.Create(passwdFile + ".tmp")
		if err != nil {
			return err
		}
		defer out.Close()

		if err := util.SetFileModeForFile(out, 0600); err != nil {
			return err
		}

		return csv.NewWriter(out).WriteAll(records)
	}()
	if err != nil {
		return err
	}

	return os.Rename(passwdFile+".tmp", passwdFile)
}