package ledger

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

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/ledger/storage/model"
	"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/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/bson/primitive"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
)

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
		}
		if req == nil {
			return nil, merrors.InvalidArgument("request is required")
		}

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

		accountCode := strings.TrimSpace(req.GetAccountCode())
		if accountCode == "" {
			return nil, merrors.InvalidArgument("account_code is required")
		}
		accountCode = strings.ToLower(accountCode)

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

		modelType, err := protoAccountTypeToModel(req.GetAccountType())
		if err != nil {
			return nil, 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 nil, err
		}

		if !req.GetIsSettlement() {
			if _, err := s.ensureSettlementAccount(ctx, orgRef, currency); err != nil {
				return nil, err
			}
		}

		metadata := req.GetMetadata()
		if len(metadata) == 0 {
			metadata = nil
		}

		describable := describableFromProto(req.GetDescribable())

		account := &model.Account{
			AccountCode:   accountCode,
			Currency:      currency,
			AccountType:   modelType,
			Status:        modelStatus,
			AllowNegative: req.GetAllowNegative(),
			IsSettlement:  req.GetIsSettlement(),
			Metadata:      metadata,
		}
		if describable != nil {
			account.Describable = *describable
		}
		account.OrganizationRef = orgRef

		err = s.storage.Accounts().Create(ctx, account)
		if err != nil {
			if errors.Is(err, merrors.ErrDataConflict) {
				existing, lookupErr := s.storage.Accounts().GetByAccountCode(ctx, orgRef, accountCode, currency)
				if lookupErr != nil {
					s.logger.Warn("duplicate account create but failed to load existing",
						zap.Error(lookupErr),
						mzap.ObjRef("organization_ref", orgRef),
						zap.String("accountCode", accountCode),
						zap.String("currency", currency))
					return nil, merrors.Internal("failed to load existing account after conflict")
				}
				recordAccountOperation("create", "duplicate")
				return &ledgerv1.CreateAccountResponse{
					Account: toProtoAccount(existing),
				}, nil
			}
			recordAccountOperation("create", "error")
			s.logger.Warn("failed to create account",
				zap.Error(err),
				mzap.ObjRef("organization_ref", orgRef),
				zap.String("accountCode", accountCode),
				zap.String("currency", currency))
			return nil, merrors.Internal("failed to create account")
		}

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

func protoAccountTypeToModel(t ledgerv1.AccountType) (model.AccountType, error) {
	switch t {
	case ledgerv1.AccountType_ACCOUNT_TYPE_ASSET:
		return model.AccountTypeAsset, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY:
		return model.AccountTypeLiability, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE:
		return model.AccountTypeRevenue, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE:
		return model.AccountTypeExpense, nil
	case ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED:
		return "", merrors.InvalidArgument("account_type is required")
	default:
		return "", merrors.InvalidArgument("invalid account_type")
	}
}

func modelAccountTypeToProto(t model.AccountType) ledgerv1.AccountType {
	switch t {
	case model.AccountTypeAsset:
		return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET
	case model.AccountTypeLiability:
		return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY
	case model.AccountTypeRevenue:
		return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE
	case model.AccountTypeExpense:
		return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE
	default:
		return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
	}
}

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

func modelAccountStatusToProto(s model.AccountStatus) ledgerv1.AccountStatus {
	switch s {
	case model.AccountStatusActive:
		return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
	case model.AccountStatusFrozen:
		return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
	default:
		return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
	}
}

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

	var accountRef string
	if id := account.GetID(); id != nil && !id.IsZero() {
		accountRef = id.Hex()
	}

	var organizationRef string
	if !account.OrganizationRef.IsZero() {
		organizationRef = account.OrganizationRef.Hex()
	}

	var createdAt *timestamppb.Timestamp
	if !account.CreatedAt.IsZero() {
		createdAt = timestamppb.New(account.CreatedAt)
	}

	var updatedAt *timestamppb.Timestamp
	if !account.UpdatedAt.IsZero() {
		updatedAt = timestamppb.New(account.UpdatedAt)
	}

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

	return &ledgerv1.LedgerAccount{
		LedgerAccountRef: accountRef,
		OrganizationRef:  organizationRef,
		AccountCode:      account.AccountCode,
		AccountType:      modelAccountTypeToProto(account.AccountType),
		Currency:         account.Currency,
		Status:           modelAccountStatusToProto(account.Status),
		AllowNegative:    account.AllowNegative,
		IsSettlement:     account.IsSettlement,
		Metadata:         metadata,
		CreatedAt:        createdAt,
		UpdatedAt:        updatedAt,
		Describable:      describableToProto(account.Describable),
	}
}

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 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 primitive.ObjectID, currency string) (*model.Account, error) {
	if s.storage == nil || s.storage.Accounts() == nil {
		return nil, errStorageNotInitialized
	}
	normalizedCurrency := strings.ToUpper(strings.TrimSpace(currency))
	if normalizedCurrency == "" {
		return nil, merrors.InvalidArgument("currency is required")
	}

	account, err := s.storage.Accounts().GetDefaultSettlement(ctx, orgRef, normalizedCurrency)
	if err == nil {
		return account, nil
	}
	if !errors.Is(err, storage.ErrAccountNotFound) {
		s.logger.Warn("failed to resolve default settlement account",
			zap.Error(err),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("currency", normalizedCurrency))
		return nil, merrors.Internal("failed to resolve settlement account")
	}

	accountCode := defaultSettlementAccountCode(normalizedCurrency)
	description := "Auto-created default settlement account"
	account = &model.Account{
		AccountCode:   accountCode,
		AccountType:   model.AccountTypeAsset,
		Currency:      normalizedCurrency,
		Status:        model.AccountStatusActive,
		AllowNegative: true,
		IsSettlement:  true,
	}
	account.OrganizationRef = orgRef
	account.Name = fmt.Sprintf("Settlement %s", normalizedCurrency)
	account.Description = &description

	if err := s.storage.Accounts().Create(ctx, account); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			existing, lookupErr := s.storage.Accounts().GetDefaultSettlement(ctx, orgRef, normalizedCurrency)
			if lookupErr == nil && existing != nil {
				return existing, nil
			}
			s.logger.Warn("duplicate settlement account create but failed to load existing",
				zap.Error(lookupErr),
				mzap.ObjRef("organization_ref", orgRef),
				zap.String("currency", normalizedCurrency))
			return nil, merrors.Internal("failed to resolve settlement account after conflict")
		}
		s.logger.Warn("failed to create default settlement account",
			zap.Error(err),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("currency", normalizedCurrency),
			zap.String("accountCode", accountCode))
		return nil, merrors.Internal("failed to create settlement account")
	}

	s.logger.Info("default settlement account created",
		mzap.ObjRef("organization_ref", orgRef),
		zap.String("currency", normalizedCurrency),
		zap.String("accountCode", accountCode))
	return account, nil
}

func defaultSettlementAccountCode(currency string) string {
	cleaned := strings.ToLower(strings.TrimSpace(currency))
	if cleaned == "" {
		return "asset:settlement"
	}
	return fmt.Sprintf("asset:settlement:%s", cleaned)
}
