package accountapiimp

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	snotifications "github.com/tech/sendico/pkg/messaging/notifications/site"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/pkg/mutil/mzap"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"github.com/tech/sendico/server/interface/api/srequest"
	"github.com/tech/sendico/server/interface/api/sresponse"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func (a *AccountAPI) createOrg(ctx context.Context, sr *srequest.Signup, permissionRef bson.ObjectID) (*model.Organization, error) {
	name := strings.TrimSpace(sr.Organization.Name)
	if name == "" {
		return nil, merrors.InvalidArgument("organization name must not be empty")
	}
	if _, err := time.LoadLocation(sr.OrganizationTimeZone); err != nil {
		return nil, merrors.DataConflict(fmt.Sprintf("invalid time zone '%s' provided, error %s", sr.OrganizationTimeZone, err.Error()))
	}

	// explicitly set org ref for permission related checks as unprotected template implementation
	// is not aware of permisssions and won't set org
	orgRef := bson.NewObjectID()
	org := &model.Organization{
		OrganizationBase: model.OrganizationBase{
			PermissionBound: model.PermissionBound{
				Base: storable.Base{
					ID:        orgRef,
					CreatedAt: time.Now(),
				},
				PermissionRef: permissionRef,
				OrganizationBoundBase: model.OrganizationBoundBase{
					OrganizationRef: orgRef,
				},
			},
			Describable: model.Describable{
				Name:        name,
				Description: sr.Organization.Description,
			},
			TimeZone: sr.OrganizationTimeZone,
		},
		Members: []bson.ObjectID{},
	}
	if err := a.odb.Unprotected().Create(ctx, org); err != nil {
		a.logger.Warn("Failed to create organization", zap.Error(err))
		return nil, err
	}

	return org, nil
}

// signupHandler handles user sign up
func (a *AccountAPI) signup(r *http.Request) http.HandlerFunc {
	// Validate user input
	var sr srequest.Signup
	if err := json.NewDecoder(r.Body).Decode(&sr); err != nil {
		a.logger.Warn("Failed to decode signup request", zap.Error(err))
		return response.BadPayload(a.logger, a.Name(), err)
	}

	sr.Account.Login = strings.ToLower(strings.TrimSpace(sr.Account.Login))
	if err := a.ensureLoginAvailable(r.Context(), sr.Account.Login); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			return response.DataConflict(a.logger, "user_already_registered", "User has already been registered")
		}
		if errors.Is(err, merrors.ErrInvalidArg) {
			return response.BadPayload(a.logger, a.Name(), err)
		}
		a.logger.Warn("Failed to validate login availability", zap.Error(err), zap.String("login", sr.Account.Login))
		return response.Internal(a.logger, a.Name(), err)
	}

	newAccount := sr.Account.ToAccount()
	if res := a.accService.ValidateAccount(newAccount); res != nil {
		a.logger.Warn("Invalid signup account received", zap.Error(res), zap.String("account", newAccount.Login))
		return response.BadPayload(a.logger, a.Name(), res)
	}

	verificationToken, err := a.executeSignupTransaction(r.Context(), &sr, newAccount)
	if err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			a.logger.Warn("Failed to register account", zap.Error(err), zap.String("login", newAccount.Login))
			return response.DataConflict(a.logger, "user_already_registered", "User has already been registered")
		}
		a.logger.Warn("Failed to create new user", zap.Error(err), zap.String("login", newAccount.Login))
		return response.Internal(a.logger, a.Name(), err)
	}

	if err := a.sendWelcomeEmail(newAccount, verificationToken); err != nil {
		a.logger.Warn("Failed to send welcome email", zap.Error(err), mzap.StorableRef(newAccount))
	}
	if err := a.sendSignupNotification(newAccount, &sr); err != nil {
		a.logger.Warn("Failed to enqueue signup notification", zap.Error(err), zap.String("login", newAccount.Login))
	}

	return sresponse.SignUp(a.logger, newAccount)
}

func (a *AccountAPI) sendSignupNotification(account *model.Account, request *srequest.Signup) error {
	if account == nil || request == nil {
		return merrors.InvalidArgument("signup notification payload is empty")
	}

	signupNotification := &model.ContactRequest{
		Name:    accountNotificationName(account),
		Email:   strings.TrimSpace(account.Login),
		Company: strings.TrimSpace(request.Organization.Name),
		Topic:   model.ContactRequestTopicSignupCompleted,
	}

	return a.producer.SendMessage(snotifications.ContactRequest(a.Name(), signupNotification))
}

func (a *AccountAPI) sendAccountVerificationCompletedNotification(account *model.Account) error {
	if account == nil {
		return merrors.InvalidArgument("account verification notification payload is empty", "account")
	}

	notification := &model.ContactRequest{
		Name:  accountNotificationName(account),
		Email: strings.TrimSpace(account.Login),
		Topic: model.ContactRequestTopicAccountVerificationCompleted,
	}

	return a.producer.SendMessage(snotifications.ContactRequest(a.Name(), notification))
}

func accountNotificationName(account *model.Account) string {
	if account == nil {
		return ""
	}
	return strings.TrimSpace(strings.Join([]string{
		strings.TrimSpace(account.Name),
		strings.TrimSpace(account.LastName),
	}, " "))
}

func (a *AccountAPI) signupAvailability(r *http.Request) http.HandlerFunc {
	login := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("login")))
	if login == "" {
		return response.BadRequest(a.logger, a.Name(), "missing_login", "login query parameter is required")
	}

	err := a.ensureLoginAvailable(r.Context(), login)
	switch {
	case err == nil:
		return sresponse.SignUpAvailability(a.logger, login, true)
	case errors.Is(err, merrors.ErrDataConflict):
		return sresponse.SignUpAvailability(a.logger, login, false)
	case errors.Is(err, merrors.ErrInvalidArg):
		return response.BadPayload(a.logger, a.Name(), err)
	default:
		a.logger.Warn("Failed to check login availability", zap.Error(err), zap.String("login", login))
		return response.Internal(a.logger, a.Name(), err)
	}
}

func (a *AccountAPI) executeSignupTransaction(ctxt context.Context, sr *srequest.Signup, newAccount *model.Account) (string, error) {
	res, err := a.tf.CreateTransaction().Execute(ctxt, func(ctx context.Context) (any, error) {
		return a.signupTransactionBody(ctx, sr, newAccount)
	})
	if token, ok := res.(string); token != "" || ok {
		return token, err
	}
	return "", err
}

func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Signup, newAccount *model.Account) (any, error) {
	var orgPolicy model.PolicyDescription
	if err := a.plcdb.GetBuiltInPolicy(ctx, mservice.Organizations, &orgPolicy); err != nil {
		a.logger.Warn("Failed to fetch built-in organization policy", zap.Error(err), zap.String("login", newAccount.Login))
		return nil, err
	}

	org, err := a.createOrg(ctx, sr, orgPolicy.ID)
	if err != nil {
		a.logger.Warn("Failed to create organization", zap.Error(err))
		return nil, err
	}
	a.logger.Info("Organization created", mzap.StorableRef(org), zap.String("account", sr.Account.Login))

	if err := a.openOrgWallet(ctx, org, sr); err != nil {
		return nil, err
	}
	a.logger.Info("Organization wallet created", mzap.StorableRef(org), zap.String("account", sr.Account.Login))
	if err := a.openOrgLedgerAccount(ctx, org, sr); err != nil {
		return nil, err
	}
	a.logger.Info("Organization ledger account created", mzap.StorableRef(org), zap.String("account", sr.Account.Login))

	roleDescription, err := a.pmanager.Role().Create(ctx, org.ID, &sr.OwnerRole)
	if err != nil {
		a.logger.Warn("Failed to create owner role", zap.Error(err), zap.String("login", newAccount.Login))
		return nil, err
	}
	a.logger.Info("Organization owner role created", mzap.StorableRef(org), zap.String("account", sr.Account.Login))

	if err := a.grantAllPermissions(ctx, org.ID, roleDescription.ID, newAccount); err != nil {
		return nil, err
	}
	a.logger.Info("Organization owner role permissions granted", mzap.StorableRef(org), zap.String("account", sr.Account.Login))

	token, err := a.accService.CreateAccount(ctx, org, newAccount, roleDescription.ID)
	if err != nil {
		a.logger.Warn("Failed to create account", zap.Error(err), zap.String("login", newAccount.Login))
		return nil, err
	}
	a.logger.Info("Organization owner account registered", mzap.StorableRef(org), zap.String("account", sr.Account.Login))

	return token, nil
}

func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bson.ObjectID, roleID bson.ObjectID, newAccount *model.Account) error {
	om := a.pmanager.Permission()
	policies, err := a.plcdb.All(ctx, organizationRef)
	if err != nil {
		a.logger.Warn("Failed to fetch permissions", zap.Error(err), mzap.StorableRef(newAccount))
		return err
	}

	required := map[mservice.Type]bool{
		mservice.Organizations:  false,
		mservice.Accounts:       false,
		mservice.LedgerAccounts: false,
	}

	actions := []model.Action{model.ActionCreate, model.ActionRead, model.ActionUpdate, model.ActionDelete}
	for _, policy := range policies {
		if policy.ResourceTypes != nil {
			for _, resource := range *policy.ResourceTypes {
				if _, ok := required[resource]; ok {
					required[resource] = true
				}
			}
		}
		for _, action := range actions {
			a.logger.Debug("Adding permission", mzap.StorableRef(&policy), zap.String("action", string(action)),
				mzap.ObjRef("role_ref", roleID), mzap.ObjRef("policy_ref", policy.ID), mzap.ObjRef("organization_ref", organizationRef))
			policy := model.RolePolicy{
				Policy: model.Policy{
					OrganizationRef: organizationRef,
					DescriptionRef:  policy.ID,
					ObjectRef:       nil, // all objects are affected
					Effect:          model.ActionEffect{Action: action, Effect: model.EffectAllow},
				},
				RoleDescriptionRef: roleID,
			}
			if err := om.GrantToRole(ctx, &policy); err != nil {
				a.logger.Warn("Failed to grant permission", zap.Error(err), mzap.StorableRef(newAccount))
				return err
			}
		}
	}

	if err := om.Save(); err != nil {
		a.logger.Warn("Failed to save permissions", zap.Error(err), mzap.StorableRef(newAccount))
		return err
	}

	for resource, granted := range required {
		if !granted {
			a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", resource))
		}
	}

	return nil
}

func (a *AccountAPI) ensureLoginAvailable(ctx context.Context, login string) error {
	if strings.TrimSpace(login) == "" {
		return merrors.InvalidArgument("login must not be empty")
	}
	if _, err := a.db.GetByEmail(ctx, login); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return nil
		}
		a.logger.Warn("Failed to lookup account by login", zap.Error(err), zap.String("login", login))
		return err
	}
	return merrors.DataConflict("account already exists")
}

func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization, sr *srequest.Signup) error {
	if a.chainGateway == nil || a.chainAsset == nil {
		a.logger.Warn("Chain gateway client not configured, skipping wallet creation", mzap.StorableRef(org))
		return merrors.Internal("chain gateway client is not configured")
	}
	req := &chainv1.CreateManagedWalletRequest{
		IdempotencyKey:  uuid.NewString(),
		OrganizationRef: org.ID.Hex(),
		Describable: &describablev1.Describable{
			Name:        sr.CryptoWallet.Name,
			Description: sr.CryptoWallet.Description,
		},
		Asset: a.chainAsset,
		Metadata: map[string]string{
			"source": "signup",
			"login":  sr.Account.Login,
		},
	}

	resp, err := a.chainGateway.CreateManagedWallet(ctx, req)
	if err != nil {
		a.logger.Warn("Failed to create managed wallet for organization", zap.Error(err), mzap.StorableRef(org))
		return err
	}
	if resp == nil || resp.Wallet == nil || strings.TrimSpace(resp.Wallet.WalletRef) == "" {
		return merrors.Internal("chain gateway returned empty wallet reference")
	}

	a.logger.Info("Managed wallet created for organization", mzap.StorableRef(org), zap.String("wallet_ref", resp.Wallet.WalletRef))
	return nil
}

func (a *AccountAPI) openOrgLedgerAccount(ctx context.Context, org *model.Organization, sr *srequest.Signup) error {
	if a.ledgerClient == nil {
		a.logger.Warn("Ledger client not configured, skipping ledger account creation", mzap.StorableRef(org))
		return merrors.Internal("ledger client is not configured")
	}
	if a.chainAsset == nil {
		return merrors.Internal("chain gateway default asset is not configured")
	}

	var describable *describablev1.Describable
	name := strings.TrimSpace(sr.LedgerWallet.Name)
	var description *string
	if sr.LedgerWallet.Description != nil {
		trimmed := strings.TrimSpace(*sr.LedgerWallet.Description)
		if trimmed != "" {
			description = &trimmed
		}
	}
	if name != "" || description != nil {
		describable = &describablev1.Describable{
			Name:        name,
			Description: description,
		}
	}

	currencies := []string{"RUB", "USDT"}
	if chainTokenCurrency := strings.ToUpper(strings.TrimSpace(a.chainAsset.GetTokenSymbol())); chainTokenCurrency != "" {
		currencies = append(currencies, chainTokenCurrency)
	}

	seen := make(map[string]struct{}, len(currencies))
	for _, currency := range currencies {
		currency = strings.ToUpper(strings.TrimSpace(currency))
		if currency == "" {
			continue
		}
		if _, exists := seen[currency]; exists {
			continue
		}
		seen[currency] = struct{}{}

		resp, err := a.ledgerClient.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
			OrganizationRef: org.ID.Hex(),
			AccountType:     ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
			Currency:        currency,
			Status:          ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE,
			Role:            ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
			Metadata: map[string]string{
				"source": "signup",
				"login":  sr.Account.Login,
			},
			Describable: describable,
		})
		if err != nil {
			a.logger.Warn("Failed to create ledger account for organization", zap.Error(err), mzap.StorableRef(org), zap.String("currency", currency))
			return err
		}
		if resp == nil || resp.GetAccount() == nil || strings.TrimSpace(resp.GetAccount().GetLedgerAccountRef()) == "" {
			return merrors.Internal("ledger returned empty account reference")
		}

		a.logger.Info("Ledger account created for organization",
			mzap.StorableRef(org),
			zap.String("currency", currency),
			zap.String("ledger_account_ref", resp.GetAccount().GetLedgerAccountRef()))
	}

	return nil
}
