// Copyright 2020 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package smtp

import (
	"net/smtp"
	"net/textproto"
	"strings"

	"github.com/pkg/errors"
	log "unknwon.dev/clog/v2"

	"gogs.io/gogs/internal/auth"
)

// Provider contains configuration of an SMTP authentication provider.
type Provider struct {
	config *Config
}

// NewProvider creates a new SMTP authentication provider.
func NewProvider(cfg *Config) auth.Provider {
	return &Provider{
		config: cfg,
	}
}

// Authenticate queries if login/password is valid against the SMTP server,
// and returns queried information when succeeded.
func (p *Provider) Authenticate(login, password string) (*auth.ExternalAccount, error) {
	// Verify allowed domains
	if p.config.AllowedDomains != "" {
		fields := strings.SplitN(login, "@", 3)
		if len(fields) != 2 {
			return nil, auth.ErrBadCredentials{Args: map[string]any{"login": login}}
		}
		domain := fields[1]

		isAllowed := false
		for _, allowed := range strings.Split(p.config.AllowedDomains, ",") {
			if domain == allowed {
				isAllowed = true
				break
			}
		}

		if !isAllowed {
			return nil, auth.ErrBadCredentials{Args: map[string]any{"login": login}}
		}
	}

	var smtpAuth smtp.Auth
	switch p.config.Auth {
	case Plain:
		smtpAuth = smtp.PlainAuth("", login, password, p.config.Host)
	case Login:
		smtpAuth = &smtpLoginAuth{login, password}
	default:
		return nil, errors.Errorf("unsupported SMTP authentication type %q", p.config.Auth)
	}

	if err := p.config.doAuth(smtpAuth); err != nil {
		log.Trace("SMTP: Authentication failed: %v", err)

		// Check standard error format first, then fallback to the worse case.
		tperr, ok := err.(*textproto.Error)
		if (ok && tperr.Code == 535) ||
			strings.Contains(err.Error(), "Username and Password not accepted") {
			return nil, auth.ErrBadCredentials{Args: map[string]any{"login": login}}
		}
		return nil, err
	}

	username := login

	// NOTE: It is not required to have "@" in `login` for a successful SMTP authentication.
	idx := strings.Index(login, "@")
	if idx > -1 {
		username = login[:idx]
	}

	return &auth.ExternalAccount{
		Login: login,
		Name:  username,
		Email: login,
	}, nil
}

func (p *Provider) Config() any {
	return p.config
}

func (*Provider) HasTLS() bool {
	return true
}

func (p *Provider) UseTLS() bool {
	return p.config.TLS
}

func (p *Provider) SkipTLSVerify() bool {
	return p.config.SkipVerify
}

const (
	Plain = "PLAIN"
	Login = "LOGIN"
)

var AuthTypes = []string{Plain, Login}

type smtpLoginAuth struct {
	username, password string
}

func (auth *smtpLoginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
	return "LOGIN", []byte(auth.username), nil
}

func (auth *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
	if more {
		switch string(fromServer) {
		case "Username:":
			return []byte(auth.username), nil
		case "Password:":
			return []byte(auth.password), nil
		}
	}
	return nil, nil
}
