package routers

import (
	"context"
	"crypto/subtle"
	"encoding/json"
	"errors"
	"net/http"
	"strings"

	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/pkg/mutil/mask"
	"github.com/tech/sendico/server/interface/api/srequest"
	"github.com/tech/sendico/server/interface/api/sresponse"
	"github.com/tech/sendico/server/internal/api/routers/ipguard"
	"go.uber.org/zap"
)

const pendingLoginTTLMinutes = 10
const apiLoginGrantType = "password"
const apiLoginClientAuthMethod = "client_secret_post"

func (pr *PublicRouter) authenticateAccount(ctx context.Context, req *srequest.Login) (*model.Account, http.HandlerFunc) {
	// Get the account database entry
	trimmedLogin := strings.TrimSpace(req.Login)
	account, err := pr.db.GetByEmail(ctx, strings.ToLower(trimmedLogin))
	if errors.Is(err, merrors.ErrNoData) || (account == nil) {
		pr.logger.Debug("User not found while logging in", zap.Error(err), zap.String("login", req.Login))
		return nil, response.Unauthorized(pr.logger, pr.service, "user not found")
	}
	if err != nil {
		pr.logger.Warn("Failed to query user with email", zap.Error(err), zap.String("login", req.Login))
		return nil, response.Internal(pr.logger, pr.service, err)
	}

	if !account.IsActive() {
		return nil, response.Forbidden(pr.logger, pr.service, "account_not_verified", "Account verification required")
	}

	if !account.MatchPassword(req.Password) {
		return nil, response.Unauthorized(pr.logger, pr.service, "password does not match")
	}

	return account, nil
}

func (pr *PublicRouter) respondPendingLogin(account *model.Account) http.HandlerFunc {
	pendingToken, err := pr.imp.CreatePendingToken(account, pendingLoginTTLMinutes)
	if err != nil {
		pr.logger.Warn("Failed to generate pending token", zap.Error(err))
		return response.Internal(pr.logger, pr.service, err)
	}

	return sresponse.LoginPending(pr.logger, account, &pendingToken, mask.Email(account.Login))
}

func hasGrantType(grants []string, target string) bool {
	for _, grant := range grants {
		if strings.EqualFold(strings.TrimSpace(grant), target) {
			return true
		}
	}
	return false
}

func (pr *PublicRouter) validateClientIPPolicy(r *http.Request, clientID string, client *model.Client) http.HandlerFunc {
	if client == nil {
		pr.logger.Info("Client not found, rejecting authorization", zap.String("client_id", clientID))
		return response.Unauthorized(pr.logger, pr.service, "client not found")
	}
	clientIP := ipguard.ClientIP(r)
	allowed, err := ipguard.Allowed(clientIP, client.AllowedCIDRs)
	if err != nil {
		pr.logger.Warn("Client IP policy contains invalid CIDR", zap.Error(err), zap.String("client_id", clientID))
		return response.Forbidden(pr.logger, pr.service, "client_ip_policy_invalid", "client ip policy is invalid")
	}
	if !allowed {
		rawIP := ""
		if clientIP != nil {
			rawIP = clientIP.String()
		}
		pr.logger.Warn("Client IP policy denied request", zap.String("client_id", clientID), zap.String("remote_ip", rawIP))
		return response.Forbidden(pr.logger, pr.service, "ip_not_allowed", "request ip is not allowed for this client")
	}
	return nil
}

func (pr *PublicRouter) validateAPIClient(ctx context.Context, r *http.Request, req *srequest.Login, account *model.Account) http.HandlerFunc {
	client, err := pr.rtdb.GetClient(ctx, req.ClientID)
	if errors.Is(err, merrors.ErrNoData) || client == nil {
		pr.logger.Debug("API login rejected: client not found", zap.String("client_id", req.ClientID))
		return response.Unauthorized(pr.logger, pr.service, "client not found")
	}
	if err != nil {
		pr.logger.Warn("API login rejected: failed to load client", zap.Error(err), zap.String("client_id", req.ClientID))
		return response.Internal(pr.logger, pr.service, err)
	}
	if client.IsRevoked {
		return response.Forbidden(pr.logger, pr.service, "client_revoked", "client has been revoked")
	}
	if !hasGrantType(client.GrantTypes, apiLoginGrantType) {
		return response.Forbidden(pr.logger, pr.service, "client_grant_not_allowed", "client does not allow password grant")
	}
	method := strings.ToLower(strings.TrimSpace(client.TokenEndpointAuthMethod))
	if method == "" {
		method = apiLoginClientAuthMethod
	}
	if method != apiLoginClientAuthMethod {
		return response.Forbidden(pr.logger, pr.service, "client_auth_method_unsupported", "unsupported client auth method")
	}

	storedSecret := strings.TrimSpace(client.ClientSecret)
	if storedSecret == "" {
		return response.Forbidden(pr.logger, pr.service, "client_secret_missing", "client secret is not configured")
	}
	if subtle.ConstantTimeCompare([]byte(storedSecret), []byte(req.ClientSecret)) != 1 {
		pr.logger.Debug("API login rejected: invalid client secret", zap.String("client_id", req.ClientID))
		return response.Unauthorized(pr.logger, pr.service, "invalid client secret")
	}
	if client.AccountRef != nil {
		accountRef := account.GetID()
		if accountRef == nil || *client.AccountRef != *accountRef {
			return response.Forbidden(pr.logger, pr.service, "client_account_mismatch", "client is bound to another account")
		}
	}
	if h := pr.validateClientIPPolicy(r, req.ClientID, client); h != nil {
		return h
	}

	return nil
}

func (pr *PublicRouter) respondAPILogin(ctx context.Context, r *http.Request, req *srequest.Login, account *model.Account) http.HandlerFunc {
	if req.ClientID == "" || req.DeviceID == "" {
		return response.BadRequest(pr.logger, pr.service, "missing_session", "session identifier is required")
	}
	accessToken, err := pr.imp.CreateAccessTokenForClient(account, req.ClientID)
	if err != nil {
		pr.logger.Warn("Failed to generate access token for API login", zap.Error(err))
		return response.Internal(pr.logger, pr.service, err)
	}
	return pr.refreshAndRespondLogin(ctx, r, &req.SessionIdentifier, account, &accessToken)
}

func decodeLogin(r *http.Request, logger mlogger.Logger) (*srequest.Login, http.HandlerFunc) {
	var req srequest.Login
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		logger.Info("Failed to decode login request", zap.Error(err))
		return nil, response.BadPayload(logger, mservice.Accounts, err)
	}
	req.Login = strings.TrimSpace(req.Login)
	req.Password = strings.TrimSpace(req.Password)
	req.ClientID = strings.TrimSpace(req.ClientID)
	req.DeviceID = strings.TrimSpace(req.DeviceID)
	req.ClientSecret = strings.TrimSpace(req.ClientSecret)

	if req.Login == "" {
		return nil, response.BadRequest(logger, mservice.Accounts, "email_missing", "login request has no user name")
	}
	if req.Password == "" {
		return nil, response.BadRequest(logger, mservice.Accounts, "password_missing", "login request has no password")
	}
	return &req, nil
}

func (a *PublicRouter) login(r *http.Request) http.HandlerFunc {
	// TODO: add rate check
	req, h := decodeLogin(r, a.logger)
	if h != nil {
		return h
	}
	account, h := a.authenticateAccount(r.Context(), req)
	if h != nil {
		return h
	}
	return a.respondPendingLogin(account)
}

func (a *PublicRouter) apiLogin(r *http.Request) http.HandlerFunc {
	req, h := decodeLogin(r, a.logger)
	if h != nil {
		return h
	}
	if req.ClientID == "" {
		return response.BadRequest(a.logger, mservice.Accounts, "client_id_missing", "clientId is required")
	}
	if req.ClientSecret == "" {
		return response.BadRequest(a.logger, mservice.Accounts, "client_secret_missing", "clientSecret is required")
	}
	account, h := a.authenticateAccount(r.Context(), req)
	if h != nil {
		return h
	}
	if h = a.validateAPIClient(r.Context(), r, req, account); h != nil {
		return h
	}
	return a.respondAPILogin(r.Context(), r, req, account)
}
