// Package util haters gonna hate.
package util

import (
	"bufio"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"syscall"
	"time"

	"github.com/apex/up/internal/colors"
	"github.com/pascaldekloe/name"
	"github.com/pkg/errors"
	"github.com/tj/backoff"
	"github.com/tj/go-progress"
	"github.com/tj/go/term"
	"golang.org/x/net/publicsuffix"
)

// ClearHeader removes all content header fields.
func ClearHeader(h http.Header) {
	h.Del("Content-Type")
	h.Del("Content-Length")
	h.Del("Content-Encoding")
	h.Del("Content-Range")
	h.Del("Content-MD5")
	h.Del("Cache-Control")
	h.Del("ETag")
	h.Del("Last-Modified")
}

// ManagedByUp appends "Managed by Up".
func ManagedByUp(s string) string {
	if s == "" {
		return "Managed by Up."
	}

	return s + " (Managed by Up)."
}

// Exists returns true if the file exists.
func Exists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

// ReadFileJSON reads json from the given path.
func ReadFileJSON(path string, v interface{}) error {
	b, err := ioutil.ReadFile(path)
	if err != nil {
		return errors.Wrap(err, "reading")
	}

	if err := json.Unmarshal(b, &v); err != nil {
		return errors.Wrap(err, "unmarshaling")
	}

	return nil
}

// Camelcase string with optional args.
func Camelcase(s string, v ...interface{}) string {
	return name.CamelCase(fmt.Sprintf(s, v...), true)
}

// NewProgressInt with the given total.
func NewProgressInt(total int) *progress.Bar {
	b := progress.NewInt(total)
	b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
	b.Width = 35
	b.StartDelimiter = colors.Gray("|")
	b.EndDelimiter = colors.Gray("|")
	b.Filled = colors.Purple("█")
	b.Empty = colors.Gray("░")
	return b
}

// NewInlineProgressInt with the given total.
func NewInlineProgressInt(total int) *progress.Bar {
	b := progress.NewInt(total)
	b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
	b.Width = 20
	b.StartDelimiter = colors.Gray("|")
	b.EndDelimiter = colors.Gray("|")
	b.Filled = colors.Purple("█")
	b.Empty = colors.Gray(" ")
	return b
}

// Pad helper.
func Pad() func() {
	println()
	return func() {
		println()
	}
}

// Fatal error.
func Fatal(err error) {
	fmt.Fprintf(os.Stderr, "\n     %s %s\n\n", colors.Red("Error:"), err)
	os.Exit(1)
}

// IsJSON returns true if the string looks like json.
func IsJSON(s string) bool {
	return len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}'
}

// IsJSONLog returns true if the string looks likes a json log.
func IsJSONLog(s string) bool {
	return IsJSON(s) && strings.Contains(s, `"level"`)
}

// IsNotFound returns true if err is not nil and represents a missing resource.
func IsNotFound(err error) bool {
	switch {
	case err == nil:
		return false
	case strings.Contains(err.Error(), "ResourceNotFoundException"):
		return true
	case strings.Contains(err.Error(), "NoSuchEntity"):
		return true
	case strings.Contains(err.Error(), "does not exist"):
		return true
	case strings.Contains(err.Error(), "not found"):
		return true
	default:
		return false
	}
}

// IsBucketExists returns true if err is not nil and represents an existing bucket.
func IsBucketExists(err error) bool {
	switch {
	case err == nil:
		return false
	case strings.Contains(err.Error(), "BucketAlreadyOwnedByYou"):
		return true
	default:
		return false
	}
}

// IsThrottled returns true if err is not nil and represents a throttled request.
func IsThrottled(err error) bool {
	switch {
	case err == nil:
		return false
	case strings.Contains(err.Error(), "Throttling: Rate exceeded"):
		return true
	default:
		return false
	}
}

// IsNoCredentials returns true if err is not nil and represents missing credentials.
func IsNoCredentials(err error) bool {
	switch {
	case err == nil:
		return false
	case strings.Contains(err.Error(), "NoCredentialProviders"):
		return true
	default:
		return false
	}
}

// Env returns a slice from environment variable map.
func Env(m map[string]string) (env []string) {
	for k, v := range m {
		env = append(env, fmt.Sprintf("%s=%s", k, v))
	}
	return
}

// PrefixLines prefixes the lines in s with prefix.
func PrefixLines(s string, prefix string) string {
	lines := strings.Split(s, "\n")
	for i, l := range lines {
		lines[i] = prefix + l
	}
	return strings.Join(lines, "\n")
}

// Indent the given string.
func Indent(s string) string {
	return PrefixLines(s, "  ")
}

// WaitForListen blocks until `u` is listening with timeout.
func WaitForListen(u *url.URL, timeout time.Duration) error {
	timedout := time.After(timeout)

	b := backoff.Backoff{
		Min:    100 * time.Millisecond,
		Max:    time.Second,
		Factor: 1.5,
	}

	for {
		select {
		case <-timedout:
			return errors.Errorf("timed out after %s", timeout)
		case <-time.After(b.Duration()):
			if IsListening(u) {
				return nil
			}
		}
	}
}

// IsListening returns true if there's a server listening on `u`.
func IsListening(u *url.URL) bool {
	conn, err := net.Dial("tcp", u.Host)
	if err != nil {
		return false
	}

	conn.Close()
	return true
}

// ExitStatus returns the exit status of cmd.
func ExitStatus(cmd *exec.Cmd, err error) string {
	ps := cmd.ProcessState

	if e, ok := err.(*exec.ExitError); ok {
		ps = e.ProcessState
	}

	if ps != nil {
		s, ok := ps.Sys().(syscall.WaitStatus)
		if ok {
			return fmt.Sprintf("%d", s.ExitStatus())
		}
	}

	return "?"
}

// StringsContains returns true if list contains s.
func StringsContains(list []string, s string) bool {
	for _, v := range list {
		if v == s {
			return true
		}
	}
	return false
}

// BasePath returns a normalized base path,
// stripping the leading '/' if present.
func BasePath(s string) string {
	return strings.TrimLeft(s, "/")
}

// LogPad outputs a log message with padding.
func LogPad(msg string, v ...interface{}) {
	defer Pad()()
	Log(msg, v...)
}

// Log outputs a log message.
func Log(msg string, v ...interface{}) {
	fmt.Printf("     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
}

// LogClear clears the line and outputs a log message.
func LogClear(msg string, v ...interface{}) {
	term.MoveUp(1)
	term.ClearLine()
	fmt.Printf("\r     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
}

// LogTitle outputs a log title.
func LogTitle(msg string, v ...interface{}) {
	fmt.Printf("\n     \x1b[1m%s\x1b[m\n\n", fmt.Sprintf(msg, v...))
}

// LogName outputs a log message with name.
func LogName(name, msg string, v ...interface{}) {
	fmt.Printf("     %s %s\n", colors.Purple(name+":"), fmt.Sprintf(msg, v...))
}

// LogListItem outputs a list item.
func LogListItem(msg string, v ...interface{}) {
	fmt.Printf("      • %s\n", fmt.Sprintf(msg, v...))
}

// ToFloat returns a float or NaN.
func ToFloat(v interface{}) float64 {
	switch n := v.(type) {
	case int:
		return float64(n)
	case int8:
		return float64(n)
	case int16:
		return float64(n)
	case int32:
		return float64(n)
	case int64:
		return float64(n)
	case uint:
		return float64(n)
	case uint8:
		return float64(n)
	case uint16:
		return float64(n)
	case uint32:
		return float64(n)
	case uint64:
		return float64(n)
	case float32:
		return float64(n)
	case float64:
		return n
	default:
		return math.NaN()
	}
}

// Milliseconds returns the duration as milliseconds.
func Milliseconds(d time.Duration) int {
	return int(d / time.Millisecond)
}

// MillisecondsSince returns the duration as milliseconds relative to time t.
func MillisecondsSince(t time.Time) int {
	return int(time.Since(t) / time.Millisecond)
}

// ParseDuration string with day and month approximation support.
func ParseDuration(s string) (d time.Duration, err error) {
	r := strings.NewReader(s)

	switch {
	case strings.HasSuffix(s, "d"):
		var v float64
		_, err = fmt.Fscanf(r, "%fd", &v)
		d = time.Duration(v * float64(24*time.Hour))
	case strings.HasSuffix(s, "w"):
		var v float64
		_, err = fmt.Fscanf(r, "%fw", &v)
		d = time.Duration(v * float64(24*time.Hour*7))
	case strings.HasSuffix(s, "mo"):
		var v float64
		_, err = fmt.Fscanf(r, "%fmo", &v)
		d = time.Duration(v * float64(30*24*time.Hour))
	case strings.HasSuffix(s, "M"):
		var v float64
		_, err = fmt.Fscanf(r, "%fM", &v)
		d = time.Duration(v * float64(30*24*time.Hour))
	default:
		d, err = time.ParseDuration(s)
	}

	return
}

// Md5 returns an md5 hash for s.
func Md5(s string) string {
	h := md5.New()
	h.Write([]byte(s))
	return hex.EncodeToString(h.Sum(nil))
}

// Domain returns the effective domain (TLD plus one).
func Domain(s string) string {
	d, err := publicsuffix.EffectiveTLDPlusOne(s)
	if err != nil {
		panic(errors.Wrap(err, "effective domain"))
	}

	return d
}

// CertDomainNames returns the certificate domain name
// and alternative names for a requested domain.
func CertDomainNames(s string) []string {
	// effective domain
	if Domain(s) == s {
		return []string{s, "*." + s}
	}

	// subdomain
	return []string{RemoveSubdomains(s, 1), "*." + RemoveSubdomains(s, 1)}
}

// IsWildcardDomain returns true if the domain is a wildcard.
func IsWildcardDomain(s string) bool {
	return strings.HasPrefix(s, "*.")
}

// WildcardMatches returns true if wildcard is a wildcard domain
// and it satisfies the given domain.
func WildcardMatches(wildcard, domain string) bool {
	if !IsWildcardDomain(wildcard) {
		return false
	}

	w := RemoveSubdomains(wildcard, 1)
	d := RemoveSubdomains(domain, 1)
	return w == d
}

// RemoveSubdomains returns the domain without the n left-most subdomain(s).
func RemoveSubdomains(s string, n int) string {
	domains := strings.Split(s, ".")
	return strings.Join(domains[n:], ".")
}

// ParseSections returns INI style sections from r.
func ParseSections(r io.Reader) (sections []string, err error) {
	s := bufio.NewScanner(r)

	for s.Scan() {
		t := s.Text()
		if strings.HasPrefix(t, "[") {
			sections = append(sections, strings.Trim(t, "[]"))
		}
	}

	err = s.Err()

	return
}

// UniqueStrings returns a string slice of unique values.
func UniqueStrings(s []string) (v []string) {
	m := make(map[string]struct{})
	for _, val := range s {
		_, ok := m[val]
		if !ok {
			v = append(v, val)
			m[val] = struct{}{}
		}
	}
	return
}

// IsCI returns true if the env looks like it's a CI platform.
func IsCI() bool {
	return os.Getenv("CI") == "true"
}

// EncodeAlias encodes an alias string so that it conforms to the
// requirement of matching (?!^[0-9]+$)([a-zA-Z0-9-_]+).
func EncodeAlias(s string) string {
	return "commit-" + strings.Replace(s, ".", "_", -1)
}

// DecodeAlias decodes an alias string which was encoded by
// the EncodeAlias function.
func DecodeAlias(s string) string {
	s = strings.Replace(s, "_", ".", -1)
	s = strings.Replace(s, "commit-", "", 1)
	return s
}

// DateSuffix returns the date suffix for t.
func DateSuffix(t time.Time) string {
	switch t.Day() {
	case 1, 21, 31:
		return "st"
	case 2, 22:
		return "nd"
	case 3, 23:
		return "rd"
	default:
		return "th"
	}
}

// StripLerna strips the owner portion of a Lerna-based tag. See #670 for
// details. They are in the form of "@owner/repo@0.5.0".
func StripLerna(s string) string {
	if strings.HasPrefix(s, "@") {
		p := strings.Split(s, "@")
		return p[len(p)-1]
	}

	return s
}

// FixMultipleSetCookie staggers the casing of each set-cookie
// value to trick API Gateway into setting multiple in the response.
func FixMultipleSetCookie(h http.Header) {
	cookies := h["Set-Cookie"]

	if len(cookies) == 0 {
		return
	}

	h.Del("Set-Cookie")

	for i, v := range cookies {
		h[BinaryCase("set-cookie", i)] = []string{v}
	}
}

// BinaryCase ported from https://github.com/Gi60s/binary-case/blob/master/index.js#L86.
func BinaryCase(s string, n int) string {
	var res []rune

	for _, c := range s {
		if c >= 65 && c <= 90 {
			if n&1 > 0 {
				c += 32
			}
			res = append(res, c)
			n >>= 1
		} else if c >= 97 && c <= 122 {
			if n&1 > 0 {
				c -= 32
			}
			res = append(res, c)
			n >>= 1
		} else {
			res = append(res, c)
		}
	}

	return string(res)
}
