package ledger

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	pmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	"github.com/tech/sendico/pkg/mutil/mzap"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// createAccountParams holds validated and normalized fields from a CreateAccountRequest.
type createAccountParams struct {
	orgRef      bson.ObjectID
	currency    string
	modelType   pmodel.LedgerAccountType
	modelStatus pmodel.LedgerAccountStatus
	modelRole   account_role.AccountRole
}

const defaultLedgerAccountName = "Ledger account"

// validateCreateAccountInput validates and normalizes all fields from the request.
func validateCreateAccountInput(req *ledgerv1.CreateAccountRequest) (createAccountParams, error) {
	if req == nil {
		return createAccountParams{}, merrors.InvalidArgument("request is required")
	}

	orgRefStr := strings.TrimSpace(req.GetOrganizationRef())
	if orgRefStr == "" {
		return createAccountParams{}, merrors.InvalidArgument("organization_ref is required")
	}
	orgRef, err := parseObjectID(orgRefStr)
	if err != nil {
		return createAccountParams{}, err
	}

	currency := strings.TrimSpace(req.GetCurrency())
	if currency == "" {
		return createAccountParams{}, merrors.InvalidArgument("currency is required")
	}

	modelType, err := protoAccountTypeToModel(req.GetAccountType())
	if err != nil {
		return createAccountParams{}, err
	}

	status := req.GetStatus()
	if status == ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED {
		status = ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
	}
	modelStatus, err := protoAccountStatusToModel(status)
	if err != nil {
		return createAccountParams{}, err
	}

	modelRole, err := protoAccountRoleToModel(req.GetRole())
	if err != nil {
		return createAccountParams{}, err
	}

	return createAccountParams{
		orgRef:      orgRef,
		currency:    strings.ToUpper(currency),
		modelType:   modelType,
		modelStatus: modelStatus,
		modelRole:   modelRole,
	}, nil
}

func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.CreateAccountRequest) gsresponse.Responder[ledgerv1.CreateAccountResponse] {
	return func(ctx context.Context) (*ledgerv1.CreateAccountResponse, error) {
		if s.storage == nil {
			return nil, errStorageNotInitialized
		}

		p, err := validateCreateAccountInput(req)
		if err != nil {
			return nil, err
		}

		// Operating accounts are user-facing and can coexist with topology accounts.
		// Ensure topology exists first, then create a dedicated account.
		if p.modelRole == account_role.AccountRoleOperating {
			if err := s.ensureLedgerTopology(ctx, p.orgRef, p.currency); err != nil {
				recordAccountOperation("create", "error")
				return nil, err
			}
			return s.persistNewAccount(ctx, p, req)
		}

		// Other topology roles resolve to existing system accounts.
		if isRequiredTopologyRole(p.modelRole) {
			return s.resolveTopologyAccount(ctx, p.orgRef, p.currency, p.modelRole)
		}

		// Non-settlement accounts require a settlement account to exist first.
		if p.modelRole != account_role.AccountRoleSettlement {
			if _, err := s.ensureSettlementAccount(ctx, p.orgRef, p.currency); err != nil {
				return nil, err
			}
		}

		return s.persistNewAccount(ctx, p, req)
	}
}

// resolveTopologyAccount ensures ledger topology is initialized and returns the system account for the given role.
func (s *Service) resolveTopologyAccount(ctx context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*ledgerv1.CreateAccountResponse, error) {
	if err := s.ensureLedgerTopology(ctx, orgRef, currency); err != nil {
		recordAccountOperation("create", "error")
		return nil, err
	}

	account, err := s.storage.Accounts().GetByRole(ctx, orgRef, currency, role)
	if err != nil {
		recordAccountOperation("create", "error")
		if errors.Is(err, storage.ErrAccountNotFound) {
			s.logger.Warn("System ledger account missing after topology ensure",
				mzap.ObjRef("organization_ref", orgRef),
				zap.String("currency", currency),
				zap.String("role", string(role)))
			return nil, merrors.Internal("failed to resolve ledger account after topology ensure")
		}
		return nil, err
	}

	recordAccountOperation("create", "success")
	return &ledgerv1.CreateAccountResponse{Account: toProtoAccount(account)}, nil
}

// persistNewAccount builds and persists a new ledger account, retrying on conflict.
func (s *Service) persistNewAccount(ctx context.Context, p createAccountParams, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
	ownerRef, err := parseOwnerRef(req.GetOwnerRef())
	if err != nil {
		return nil, err
	}

	metadata := req.GetMetadata()
	if len(metadata) == 0 {
		metadata = nil
	}
	describable := ensureDefaultLedgerAccountName(describableFromProto(req.GetDescribable()))

	const maxCreateAttempts = 3
	for attempt := 0; attempt < maxCreateAttempts; attempt++ {
		accountID := bson.NewObjectID()
		account := buildNewAccount(p, metadata, describable, ownerRef, req.GetAllowNegative(), accountID)

		err := s.storage.Accounts().Create(ctx, account)
		if err == nil {
			s.logger.Info("Created ledger account",
				mzap.ObjRef("organization_ref", p.orgRef),
				zap.String("account_code", account.AccountCode),
				zap.String("currency", p.currency),
				zap.String("role", string(p.modelRole)))
			recordAccountOperation("create", "success")
			return &ledgerv1.CreateAccountResponse{Account: toProtoAccount(account)}, nil
		}

		if errors.Is(err, merrors.ErrDataConflict) && attempt < maxCreateAttempts-1 {
			continue
		}

		recordAccountOperation("create", "error")
		s.logger.Warn("Failed to create account", zap.Error(err),
			mzap.ObjRef("organization_ref", p.orgRef),
			zap.String("account_code", account.AccountCode),
			zap.String("currency", p.currency))
		return nil, merrors.Internal("failed to create account")
	}

	recordAccountOperation("create", "error")
	return nil, merrors.Internal("failed to create account after retries")
}

// parseOwnerRef parses an optional owner reference string into an ObjectID pointer.
func parseOwnerRef(ownerRefStr string) (*bson.ObjectID, error) {
	if ownerRefStr == "" {
		return nil, nil //nolint:nilnil // empty owner_ref means owner linkage is intentionally omitted
	}
	ownerObjID, err := parseObjectID(ownerRefStr)
	if err != nil {
		return nil, merrors.InvalidArgument(ownerRefStr, "owner_ref")
	}
	return &ownerObjID, nil
}

// buildNewAccount constructs a LedgerAccount model from validated parameters.
func buildNewAccount(p createAccountParams, metadata map[string]string, describable *pmodel.Describable, ownerRef *bson.ObjectID, allowNegative bool, accountRef bson.ObjectID) *pmodel.LedgerAccount {
	account := &pmodel.LedgerAccount{
		AccountCode:   generateAccountCode(p.modelType, p.currency, accountRef),
		Currency:      p.currency,
		AccountType:   p.modelType,
		Status:        p.modelStatus,
		AllowNegative: allowNegative,
		Role:          p.modelRole,
		Metadata:      metadata,
		OwnerRef:      ownerRef,
		Scope:         pmodel.LedgerAccountScopeOrganization,
	}
	if describable != nil {
		account.Describable = *describable
	}
	account.OrganizationRef = &p.orgRef
	account.SetID(accountRef)
	return account
}

func protoAccountTypeToModel(t ledgerv1.AccountType) (pmodel.LedgerAccountType, error) {
	switch t {
	case ledgerv1.AccountType_ACCOUNT_TYPE_ASSET:
		return pmodel.LedgerAccountTypeAsset, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY:
		return pmodel.LedgerAccountTypeLiability, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE:
		return pmodel.LedgerAccountTypeRevenue, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE:
		return pmodel.LedgerAccountTypeExpense, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED:
		return "", merrors.InvalidArgument("account_type is required")
	default:
		return "", merrors.InvalidArgument("invalid account_type")
	}
}

func modelAccountTypeToProto(t pmodel.LedgerAccountType) ledgerv1.AccountType {
	switch t {
	case pmodel.LedgerAccountTypeAsset:
		return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET
	case pmodel.LedgerAccountTypeLiability:
		return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY
	case pmodel.LedgerAccountTypeRevenue:
		return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE
	case pmodel.LedgerAccountTypeExpense:
		return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE
	default:
		return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
	}
}

func protoAccountRoleToModel(r ledgerv1.AccountRole) (account_role.AccountRole, error) {
	switch r {
	case ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED:
		return account_role.AccountRoleOperating, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD:
		return account_role.AccountRoleHold, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT:
		return account_role.AccountRoleTransit, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT:
		return account_role.AccountRoleSettlement, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING:
		return account_role.AccountRoleClearing, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING:
		return account_role.AccountRolePending, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE:
		return account_role.AccountRoleReserve, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY:
		return account_role.AccountRoleLiquidity, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_FEE:
		return account_role.AccountRoleFee, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK:
		return account_role.AccountRoleChargeback, nil
	case ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT:
		return account_role.AccountRoleAdjustment, nil
	default:
		return "", merrors.InvalidArgument("invalid account role")
	}
}

func modelAccountRoleToProto(r account_role.AccountRole) ledgerv1.AccountRole {
	switch r {
	case account_role.AccountRoleOperating, "":
		return ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING
	case account_role.AccountRoleHold:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_HOLD
	case account_role.AccountRoleTransit:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_TRANSIT
	case account_role.AccountRoleSettlement:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_SETTLEMENT
	case account_role.AccountRoleClearing:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_CLEARING
	case account_role.AccountRolePending:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_PENDING
	case account_role.AccountRoleReserve:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_RESERVE
	case account_role.AccountRoleLiquidity:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_LIQUIDITY
	case account_role.AccountRoleFee:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_FEE
	case account_role.AccountRoleChargeback:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_CHARGEBACK
	case account_role.AccountRoleAdjustment:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_ADJUSTMENT
	default:
		return ledgerv1.AccountRole_ACCOUNT_ROLE_UNSPECIFIED
	}
}

func protoAccountStatusToModel(s ledgerv1.AccountStatus) (pmodel.LedgerAccountStatus, error) {
	switch s {
	case ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE:
		return pmodel.LedgerAccountStatusActive, nil
	case ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN:
		return pmodel.LedgerAccountStatusFrozen, nil
	case ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED:
		return "", merrors.InvalidArgument("account status is required")
	default:
		return "", merrors.InvalidArgument("invalid account status")
	}
}

func modelAccountStatusToProto(s pmodel.LedgerAccountStatus) ledgerv1.AccountStatus {
	switch s {
	case pmodel.LedgerAccountStatusActive:
		return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
	case pmodel.LedgerAccountStatusFrozen:
		return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
	default:
		return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
	}
}

func toProtoAccount(account *pmodel.LedgerAccount) *ledgerv1.LedgerAccount {
	if account == nil {
		return nil
	}

	metadata := account.Metadata
	if len(metadata) == 0 {
		metadata = nil
	}

	return &ledgerv1.LedgerAccount{
		LedgerAccountRef: objectIDPtrHex(account.GetID()),
		OrganizationRef:  objectIDHex(*account.OrganizationRef),
		OwnerRef:         objectIDPtrHex(account.OwnerRef),
		AccountCode:      account.AccountCode,
		AccountType:      modelAccountTypeToProto(account.AccountType),
		Currency:         account.Currency,
		Status:           modelAccountStatusToProto(account.Status),
		AllowNegative:    account.AllowNegative,
		Role:             modelAccountRoleToProto(account.Role),
		Metadata:         metadata,
		CreatedAt:        toTimestamp(account.CreatedAt),
		UpdatedAt:        toTimestamp(account.UpdatedAt),
		Describable:      describableToProto(account.Describable),
	}
}

func objectIDHex(id bson.ObjectID) string {
	if id.IsZero() {
		return ""
	}
	return id.Hex()
}

func objectIDPtrHex(id *bson.ObjectID) string {
	if id == nil || id.IsZero() {
		return ""
	}
	return id.Hex()
}

func toTimestamp(t time.Time) *timestamppb.Timestamp {
	if t.IsZero() {
		return nil
	}
	return timestamppb.New(t)
}

func describableFromProto(desc *describablev1.Describable) *pmodel.Describable {
	if desc == nil {
		return nil
	}
	name := strings.TrimSpace(desc.GetName())
	var description *string
	if desc.Description != nil {
		trimmed := strings.TrimSpace(desc.GetDescription())
		if trimmed != "" {
			description = &trimmed
		}
	}
	if name == "" && description == nil {
		return nil
	}
	return &pmodel.Describable{
		Name:        name,
		Description: description,
	}
}

func ensureDefaultLedgerAccountName(desc *pmodel.Describable) *pmodel.Describable {
	if desc == nil {
		return &pmodel.Describable{Name: defaultLedgerAccountName}
	}
	if strings.TrimSpace(desc.Name) != "" {
		return desc
	}
	copy := *desc
	copy.Name = defaultLedgerAccountName
	return &copy
}

func describableToProto(desc pmodel.Describable) *describablev1.Describable {
	name := strings.TrimSpace(desc.Name)
	var description *string
	if desc.Description != nil {
		trimmed := strings.TrimSpace(*desc.Description)
		if trimmed != "" {
			description = &trimmed
		}
	}
	if name == "" && description == nil {
		return nil
	}
	return &describablev1.Describable{
		Name:        name,
		Description: description,
	}
}

func (s *Service) ensureSettlementAccount(ctx context.Context, orgRef bson.ObjectID, currency string) (*pmodel.LedgerAccount, error) {
	return s.ensureRoleAccount(ctx, orgRef, currency, account_role.AccountRoleSettlement)
}

func generateAccountCode(accountType pmodel.LedgerAccountType, currency string, id bson.ObjectID) string {
	typePart := strings.ToLower(strings.TrimSpace(string(accountType)))
	if typePart == "" {
		typePart = "account"
	}
	currencyPart := strings.ToLower(strings.TrimSpace(currency))
	if currencyPart == "" {
		currencyPart = "na"
	}
	return fmt.Sprintf("%s:%s:%s", typePart, currencyPart, id.Hex())
}
