// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2014-2018 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package store

import (
	"errors"
	"fmt"
	"net/url"
	"sort"
	"strings"

	"github.com/snapcore/snapd/snap/channel"
	"github.com/snapcore/snapd/strutil"
)

var (
	// ErrBadQuery is returned from Find when the query has special characters in strange places.
	ErrBadQuery = errors.New("bad query")

	// ErrInvalidScope is returned from Find when an invalid scope is requested.
	ErrInvalidScope = errors.New("invalid scope")

	// ErrSnapNotFound is returned when a snap can not be found
	ErrSnapNotFound = errors.New("snap not found")

	// ErrUnauthenticated is returned when authentication is needed to complete the query
	ErrUnauthenticated = errors.New("you need to log in first")

	// ErrAuthenticationNeeds2fa is returned if the authentication needs 2factor
	ErrAuthenticationNeeds2fa = errors.New("two factor authentication required")

	// Err2faFailed is returned when 2fa failed (e.g., a bad token was given)
	Err2faFailed = errors.New("two factor authentication failed")

	// ErrInvalidCredentials is returned on login error
	// It can also be returned when refreshing the discharge
	// macaroon if the user has changed their password.
	ErrInvalidCredentials = errors.New("invalid credentials")

	// ErrTOSNotAccepted is returned when the user has not accepted the store's terms of service.
	ErrTOSNotAccepted = errors.New("terms of service not accepted")

	// ErrNoPaymentMethods is returned when the user has no valid payment methods associated with their account.
	ErrNoPaymentMethods = errors.New("no payment methods")

	// ErrPaymentDeclined is returned when the user's payment method was declined by the upstream payment provider.
	ErrPaymentDeclined = errors.New("payment declined")

	// ErrLocalSnap is returned when an operation that only applies to snaps that come from a store was attempted on a local snap.
	ErrLocalSnap = errors.New("cannot perform operation on local snap")

	// ErrNoUpdateAvailable is returned when an update is attempted for a snap that has no update available.
	ErrNoUpdateAvailable = errors.New("snap has no updates available")
)

// RevisionNotAvailableError is returned when an install is attempted for a snap but the/a revision is not available (given install constraints).
type RevisionNotAvailableError struct {
	Action   string
	Channel  string
	Releases []channel.Channel
}

func (e *RevisionNotAvailableError) Error() string {
	return "no snap revision available as specified"
}

// DownloadError represents a download error
type DownloadError struct {
	Code int
	URL  *url.URL
}

func (e *DownloadError) Error() string {
	return fmt.Sprintf("received an unexpected http response code (%v) when trying to download %s", e.Code, e.URL)
}

// PasswordPolicyError is returned in a few corner cases, most notably
// when the password has been force-reset.
type PasswordPolicyError map[string]stringList

func (e PasswordPolicyError) Error() string {
	var msg string

	if reason, ok := e["reason"]; ok && len(reason) == 1 {
		msg = reason[0]
		if location, ok := e["location"]; ok && len(location) == 1 {
			msg += "\nTo address this, go to: " + location[0] + "\n"
		}
	} else {
		for k, vs := range e {
			msg += fmt.Sprintf("%s: %s\n", k, strings.Join(vs, "  "))
		}
	}

	return msg
}

// InvalidAuthDataError signals that the authentication data didn't pass validation.
type InvalidAuthDataError map[string]stringList

func (e InvalidAuthDataError) Error() string {
	var es []string
	for _, v := range e {
		es = append(es, v...)
	}
	// XXX: confirm with server people that extra args are all
	//      full sentences (with periods and capitalization)
	//      (empirically this checks out)
	return strings.Join(es, "  ")
}

// SnapActionError conveys errors that were reported on otherwise overall successful snap action (install/refresh) request.
type SnapActionError struct {
	// NoResults is set if there were no results in the response
	NoResults bool
	// Refresh errors by snap name.
	Refresh map[string]error
	// Install errors by snap name.
	Install map[string]error
	// Download errors by snap name.
	Download map[string]error
	// Other errors.
	Other []error
}

// SingleOpError returns the single operation, snap name, and error if
// e represents a single error of a single operation on a single snap
// (i.e. if e.Other is empty, and e.Refresh, e.Install and e.Download
// have a single error in total).
// In any other case, the error returned will be nil.
func (e SnapActionError) SingleOpError() (op, name string, err error) {
	if len(e.Other) > 0 {
		return "", "", nil
	}

	nRefresh := len(e.Refresh)
	nInstall := len(e.Install)
	nDownload := len(e.Download)
	if nRefresh+nInstall+nDownload != 1 {
		return "", "", nil
	}

	var errs map[string]error
	switch {
	case nRefresh > 0:
		op = "refresh"
		errs = e.Refresh
	case nInstall > 0:
		op = "install"
		errs = e.Install
	case nDownload > 0:
		op = "download"
		errs = e.Download
	}
	for name, err = range errs {
		return op, name, err
	}
	// can't happen
	return "", "", nil
}

func (e SnapActionError) Error() string {
	nRefresh := len(e.Refresh)
	nInstall := len(e.Install)
	nDownload := len(e.Download)
	nOther := len(e.Other)

	// single error
	switch nRefresh + nInstall + nDownload + nOther {
	case 0:
		if e.NoResults {
			// this is an atypical result
			return "no install/refresh information results from the store"
		}
	case 1:
		if nOther == 0 {
			op, name, err := e.SingleOpError()
			return fmt.Sprintf("cannot %s snap %q: %v", op, name, err)
		} else {
			return fmt.Sprintf("cannot refresh, install, or download: %v", e.Other[0])
		}
	}

	header := "cannot refresh, install, or download:"
	if nOther == 0 {
		// at least one of nDownload, nInstall, or nRefresh is > 0
		switch {
		case nDownload == 0 && nRefresh == 0:
			header = "cannot install:"
		case nDownload == 0 && nInstall == 0:
			header = "cannot refresh:"
		case nRefresh == 0 && nInstall == 0:
			header = "cannot download:"
		case nDownload == 0:
			header = "cannot refresh or install:"
		case nInstall == 0:
			header = "cannot refresh or download:"
		case nRefresh == 0:
			header = "cannot install or download:"
		}
	}

	// reverse the "snap->error" map to "error->snap", as the
	// common case is that all snaps fail with the same error
	// (e.g. "no refresh available")
	errToSnaps := map[string][]string{}
	errKeys := []string{} // poorman's ordered map

	for _, m := range []map[string]error{e.Refresh, e.Install, e.Download} {
		for snapName, err := range m {
			k := err.Error()
			v, ok := errToSnaps[k]
			if !ok {
				errKeys = append(errKeys, k)
			}
			errToSnaps[k] = append(v, snapName)
		}
	}

	es := make([]string, 1, 1+len(errToSnaps)+nOther)
	es[0] = header
	for _, k := range errKeys {
		sort.Strings(errToSnaps[k])
		es = append(es, fmt.Sprintf("%s: %s", k, strutil.Quoted(errToSnaps[k])))
	}

	for _, e := range e.Other {
		es = append(es, e.Error())
	}

	if len(es) == 2 {
		// header + 1 reason
		return strings.Join(es, " ")
	}

	return strings.Join(es, "\n")
}

// Authorization soft-expiry errors that get handled automatically.
var (
	errUserAuthorizationNeedsRefresh   = errors.New("soft-expired user authorization needs refresh")
	errDeviceAuthorizationNeedsRefresh = errors.New("soft-expired device authorization needs refresh")
)

func translateSnapActionError(action, snapChannel, code, message string, releases []snapRelease) error {
	switch code {
	case "revision-not-found":
		e := &RevisionNotAvailableError{
			Action:  action,
			Channel: snapChannel,
		}
		if len(releases) != 0 {
			parsedReleases := make([]channel.Channel, len(releases))
			for i := 0; i < len(releases); i++ {
				var err error
				parsedReleases[i], err = channel.Parse(releases[i].Channel, releases[i].Architecture)
				if err != nil {
					// shouldn't happen, return error without Releases
					return e
				}
			}
			e.Releases = parsedReleases
		}
		return e
	case "id-not-found", "name-not-found":
		return ErrSnapNotFound
	case "user-authorization-needs-refresh":
		return errUserAuthorizationNeedsRefresh
	case "device-authorization-needs-refresh":
		return errDeviceAuthorizationNeedsRefresh
	default:
		return fmt.Errorf("%v", message)
	}
}
