// Package fqme provides a single function WhoAmI that tries to get
// your real name and email address by mining various configuration
// files.
//
// If you want to force the value, put a standard-format name and email
// address (latter surrounded by <>) in ~/.config/fqme/whoami.  
//
// However, this code will normally give useful results without special
// configuration, and is intended to be used that way. It looks first in
// places where you have probably explicitly declared your identity for
// use in distributed version-control systems, mail agents, browsers,
// etc. It falls back to using your GECOS name and username@host-FQDN.
//
// If you have multiple different forms of your name or multiple
// different email addresses in different config files, this code will
// not necessarily do what you expect.  It's going to return whatever
// valid-looking identity it finds first.
//
// Configuration files it looks at, in order: git, hg, bzr, mutt,
// pine/alpine lynx, passwd.  More may have been added by the time you
// read this.
//
// The name stands for "Fully Qualified Me". In case you were
// wondering.

package fqme

// SPDX-License-Identifier: BSD-2-Clause
// Copyright by Eric S. Raymond, 2019

import (
	"bufio"
	"errors"
	"io/ioutil"
	"net/mail"
	"os"
	"os/exec"
	"os/user"
	"path"
	"regexp"
	"strings"
)

const (
	haveNAME   = 0x1
	haveEMAIL  = 0x2
	haveDOMAIN = 0x04
)

type configCap struct {
	rc      string
	matcher *regexp.Regexp
	has     int
}

var configs []configCap

func init() {
	// Force regexps to be compile early, so they'll break noisily
	// if they're wrong, and require compilation only once
	configs = []configCap{
		{".muttrc", regexp.MustCompile(`set realname="([^"]*)"`), haveNAME},
		{".muttrc", regexp.MustCompile(`set from=(.*)`), haveEMAIL},
		{".muttrc", regexp.MustCompile(`my_hdr Reply-To: (.*)`), haveEMAIL},
		{".pinerc", regexp.MustCompile(`personal-name\s*=\s*"(.*)"`), haveNAME},
		{".pinerc", regexp.MustCompile(`user-domain\s*=\s*(.*)`), haveDOMAIN},
		{".lynxrc", regexp.MustCompile(`personal_mail_address\s*=\s*(.*)\s<([^>]*)>`), haveNAME | haveEMAIL},
	}
}

// WhoAmI tries to get your real name and email address by mining
// various configuration files.
//
// If you want to force the value, put a standard-format name and
// email address (latter surrounded by <>) in
// ~/.config/fqme/whoami. Otherwise, it looks first in places where you
// have probably explicitly declared an identity for use in distributed
// version-control systems, mail agents, browsers, etc. It falls back
// to using your GECOS name and username@host-FQDN.
//
// The return values are a name string, an email address, and an error
// value which is nil only if both name and email are both nonempty
// and well-formed.

func WhoAmI() (string, string, error) {
	me, err0 := user.Current()
	if err0 == nil {
		data, err := ioutil.ReadFile(path.Join(me.HomeDir, ".config", "fqme", "whoami"))
		if err == nil {
			e, err := mail.ParseAddress(strings.Trim(string(data), "\n"))
			if err != nil {
				return e.Name, e.Address, nil
			}
		}
	}
	
	// Git version-control system
	out1, err1 := exec.Command("git", "config", "user.name").CombinedOutput()
	out2, err2 := exec.Command("git", "config", "user.email").CombinedOutput()
	if err1 == nil && len(out1) != 0 && err2 == nil && len(out2) != 0 {
		return strings.Trim(string(out1), "\n"), strings.Trim(string(out2), "\n"), nil
	}

	// Mercurial version control system
	out3, err3 := exec.Command("hg", "config", "ui.username").CombinedOutput()
	if err3 == nil && len(out3) != 0 {
		e, err := mail.ParseAddress(strings.Trim(string(out3), "\n"))
		if err == nil {
			return e.Name, e.Address, nil
		}
	}

	// Bazaar version control system
	out4, err4 := exec.Command("bzr", "config", "email").CombinedOutput()
	if err4 == nil && len(out4) != 0 {
		e, err := mail.ParseAddress(strings.Trim(string(out4), "\n"))
		if err != nil {
			return e.Name, e.Address, nil
		}
	}

	nameNotBogus := func(name string) bool {
		return name != "" // Is there any other validation possible?
	}
	emailNotBogus := func(email string) bool {
		return email != "" && strings.Contains(email, "@") && strings.Contains(email, ".")
	}

	var name, email string

	// Other more obscure places
	if err0 == nil {
		for _, config := range configs {
			fn := path.Join(me.HomeDir, config.rc)
			fp, err := os.Open(fn)
			if err != nil {
				continue
			}
			rd := bufio.NewReader(fp)
			for {
				line, e := rd.ReadString(0x0A)
				if e != nil {
					break
				}
				matches := config.matcher.FindAllStringSubmatch(strings.TrimSpace(line), -1)
				if (config.has & (haveNAME | haveEMAIL)) != 0 {
					name = matches[0][1]
					email = matches[0][2]
				} else if (config.has & haveNAME) != 0 {
					name = matches[0][1]
				} else if (config.has & haveEMAIL) != 0 {
					email = matches[0][1]
				} else if (config.has & haveDOMAIN) != 0 {
					email = me.Username + "@" + matches[0][1]
				}
				if nameNotBogus(name) && emailNotBogus(email) {
					return name, email, nil
				}
			}
		}

		// No name from configs?  Use GECOS if it's there
		if name == "" {
			name = me.Name
		}

		// No email from configs? Fall back to username @ host FQDN
		fqdn, err6 := exec.Command("hostname", "--fqdn").CombinedOutput()
		if err6 == nil {
			email = me.Username + "@" + string(fqdn)
		}
	}

	// Out of alternatives - return what we have
	if nameNotBogus(name) && emailNotBogus(email) {
		return name, email, nil
	}

	// Someday we might do consistency checking
	return name, email, errors.New("ill-formed WhoAmI information")
}

// end
