package paymethodsimp

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

	methodsclient "github.com/tech/sendico/payments/methods/client"
	api "github.com/tech/sendico/pkg/api/http"
	"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"
	archivablev1 "github.com/tech/sendico/pkg/proto/common/archivable/v1"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	oboundv1 "github.com/tech/sendico/pkg/proto/common/organization_bound/v1"
	paginationv2 "github.com/tech/sendico/pkg/proto/common/pagination/v2"
	pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1"
	storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1"
	endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
	methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1"
	eapi "github.com/tech/sendico/server/interface/api"
	"github.com/tech/sendico/server/interface/api/sresponse"
	mutil "github.com/tech/sendico/server/internal/mutil/param"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"
	"google.golang.org/protobuf/types/known/wrapperspb"
)

type PaymentMethodsAPI struct {
	logger mlogger.Logger
	client methodsclient.Client
	oph    mutil.ParamHelper
	rph    mutil.ParamHelper
	mph    mutil.ParamHelper
}

func (a *PaymentMethodsAPI) Name() mservice.Type {
	return mservice.PaymentMethods
}

func (a *PaymentMethodsAPI) Finish(_ context.Context) error {
	if a.client != nil {
		return a.client.Close()
	}
	return nil
}

func CreateAPI(apiCtx eapi.API) (*PaymentMethodsAPI, error) {
	logger := apiCtx.Logger().Named(mservice.PaymentMethods)

	cfg := apiCtx.Config().PaymentMethods
	if cfg == nil {
		return nil, merrors.InvalidArgument("payment methods configuration is not provided")
	}

	address, err := resolveClientAddress("payment methods", cfg)
	if err != nil {
		return nil, err
	}

	clientCfg := methodsclient.Config{
		Address:     address,
		DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
		CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
		Insecure:    cfg.Insecure,
	}

	client, err := methodsclient.New(context.Background(), clientCfg)
	if err != nil {
		return nil, err
	}

	res := &PaymentMethodsAPI{
		logger: logger,
		client: client,
		oph:    mutil.CreatePH(mservice.Organizations),
		rph:    mutil.CreatePH(mservice.Recipients),
		mph:    mutil.CreatePH(mservice.PaymentMethods),
	}

	apiCtx.Register().AccountHandler(res.Name(), res.oph.AddRef("/"), api.Post, res.create)
	apiCtx.Register().AccountHandler(res.Name(), res.rph.AddRef(res.oph.AddRef("/list")), api.Get, res.list)
	apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef("/"), api.Get, res.get)
	apiCtx.Register().AccountHandler(res.Name(), "/", api.Put, res.update)
	apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef("/"), api.Delete, res.delete)
	apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef(res.oph.AddRef("/archive")), api.Get, res.archive)

	return res, nil
}

func (a *PaymentMethodsAPI) create(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	orgRef, err := a.oph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
	}

	payload, err := io.ReadAll(r.Body)
	if err != nil {
		return response.BadPayload(a.logger, a.Name(), err)
	}
	pm, err := decodePaymentMethodJSON(payload)
	if err != nil {
		return response.BadPayload(a.logger, a.Name(), err)
	}
	method, err := encodePaymentMethodProto(pm)
	if err != nil {
		return response.Internal(a.logger, a.Name(), err)
	}

	resp, err := a.client.CreatePaymentMethod(r.Context(), &methodsv1.CreatePaymentMethodRequest{
		AccountRef:      account.ID.Hex(),
		OrganizationRef: orgRef.Hex(),
		PaymentMethod:   method,
	})
	if err != nil {
		return grpcErrorResponse(a.logger, a.Name(), err)
	}

	pm, err = decodePaymentMethodRecord(resp.GetPaymentMethodRecord())
	if err != nil {
		return response.Internal(a.logger, a.Name(), err)
	}
	return sresponse.ObjectAuthCreated(a.logger, pm, token, a.Name())
}

func (a *PaymentMethodsAPI) list(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	orgRef, err := a.oph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
	}
	recipientRef, err := a.rph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.rph.Name(), a.rph.GetID(r), err)
	}

	cursor, err := mutil.GetViewCursor(a.logger, r)
	if err != nil {
		return response.Auto(a.logger, a.Name(), err)
	}

	resp, err := a.client.ListPaymentMethods(r.Context(), &methodsv1.ListPaymentMethodsRequest{
		AccountRef:      account.ID.Hex(),
		OrganizationRef: orgRef.Hex(),
		RecipientRef:    recipientRef.Hex(),
		Cursor:          toProtoCursor(cursor),
	})
	if err != nil {
		return grpcErrorResponse(a.logger, a.Name(), err)
	}

	items, err := decodePaymentMethods(resp.GetPaymentMethods())
	if err != nil {
		return response.Internal(a.logger, a.Name(), err)
	}
	return sresponse.ObjectsAuth(a.logger, items, token, a.Name())
}

func (a *PaymentMethodsAPI) get(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	methodRef, err := a.mph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err)
	}

	resp, err := a.client.GetPaymentMethod(r.Context(), &methodsv1.GetPaymentMethodRequest{
		AccountRef:       account.ID.Hex(),
		PaymentMethodRef: methodRef.Hex(),
	})
	if err != nil {
		return grpcErrorResponse(a.logger, a.Name(), err)
	}

	pm, err := decodePaymentMethodRecord(resp.GetPaymentMethodRecord())
	if err != nil {
		return response.Internal(a.logger, a.Name(), err)
	}
	return sresponse.ObjectAuth(a.logger, pm, token, a.Name())
}

func (a *PaymentMethodsAPI) update(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	payload, err := io.ReadAll(r.Body)
	if err != nil {
		return response.BadPayload(a.logger, a.Name(), err)
	}
	pm, err := decodePaymentMethodJSON(payload)
	if err != nil {
		return response.BadPayload(a.logger, a.Name(), err)
	}
	record, err := encodePaymentMethodRecord(pm)
	if err != nil {
		return response.Internal(a.logger, a.Name(), err)
	}

	resp, err := a.client.UpdatePaymentMethod(r.Context(), &methodsv1.UpdatePaymentMethodRequest{
		AccountRef:          account.ID.Hex(),
		PaymentMethodRecord: record,
	})
	if err != nil {
		return grpcErrorResponse(a.logger, a.Name(), err)
	}

	pm, err = decodePaymentMethodRecord(resp.GetPaymentMethodRecord())
	if err != nil {
		return response.Internal(a.logger, a.Name(), err)
	}
	return sresponse.ObjectAuth(a.logger, pm, token, a.Name())
}

func (a *PaymentMethodsAPI) delete(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	methodRef, err := a.mph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err)
	}

	cascade, err := mutil.GetCascadeParam(a.logger, r)
	if err != nil {
		return response.Auto(a.logger, a.Name(), err)
	}

	cascadeValue := false
	if cascade != nil {
		cascadeValue = *cascade
	}

	_, err = a.client.DeletePaymentMethod(r.Context(), &methodsv1.DeletePaymentMethodRequest{
		AccountRef:       account.ID.Hex(),
		PaymentMethodRef: methodRef.Hex(),
		Cascade:          cascadeValue,
	})
	if err != nil {
		return grpcErrorResponse(a.logger, a.Name(), err)
	}

	return sresponse.ObjectsAuth(a.logger, []model.PaymentMethod{}, token, a.Name())
}

func (a *PaymentMethodsAPI) archive(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
	methodRef, err := a.mph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err)
	}
	orgRef, err := a.oph.GetRef(r)
	if err != nil {
		return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
	}

	archived, err := mutil.GetArchiveParam(a.logger, r)
	if err != nil {
		return response.Auto(a.logger, a.Name(), err)
	}
	if archived == nil {
		return response.BadRequest(a.logger, a.Name(), "invalid_query_parameter", "'archived' param must be present")
	}

	cascade, err := mutil.GetCascadeParam(a.logger, r)
	if err != nil {
		return response.Auto(a.logger, a.Name(), err)
	}
	cascadeValue := false
	if cascade != nil {
		cascadeValue = *cascade
	}

	_, err = a.client.SetPaymentMethodArchived(r.Context(), &methodsv1.SetPaymentMethodArchivedRequest{
		AccountRef:       account.ID.Hex(),
		OrganizationRef:  orgRef.Hex(),
		PaymentMethodRef: methodRef.Hex(),
		Archived:         *archived,
		Cascade:          cascadeValue,
	})
	if err != nil {
		return grpcErrorResponse(a.logger, a.Name(), err)
	}

	return sresponse.ObjectsAuth(a.logger, []model.PaymentMethod{}, token, a.Name())
}

func resolveClientAddress(service string, cfg *eapi.PaymentOrchestratorConfig) (string, error) {
	if cfg == nil {
		return "", merrors.InvalidArgument(strings.TrimSpace(service) + " configuration is not provided")
	}
	address := strings.TrimSpace(cfg.Address)
	if address != "" {
		return address, nil
	}
	if env := strings.TrimSpace(cfg.AddressEnv); env != "" {
		if resolved := strings.TrimSpace(os.Getenv(env)); resolved != "" {
			return resolved, nil
		}
		return "", merrors.InvalidArgument(service + " address is not specified and address env " + env + " is empty")
	}
	return "", merrors.InvalidArgument(strings.TrimSpace(service) + " address is not specified")
}

func toProtoCursor(cursor *model.ViewCursor) *paginationv2.ViewCursor {
	if cursor == nil {
		return nil
	}

	res := &paginationv2.ViewCursor{}
	hasAny := false
	if cursor.Limit != nil {
		res.Limit = wrapperspb.Int64(*cursor.Limit)
		hasAny = true
	}
	if cursor.Offset != nil {
		res.Offset = wrapperspb.Int64(*cursor.Offset)
		hasAny = true
	}
	if cursor.IsArchived != nil {
		res.IsArchived = wrapperspb.Bool(*cursor.IsArchived)
		hasAny = true
	}
	if !hasAny {
		return nil
	}
	return res
}

func decodePaymentMethodJSON(payload []byte) (*model.PaymentMethod, error) {
	var pm model.PaymentMethod
	if err := json.Unmarshal(payload, &pm); err != nil {
		return nil, err
	}
	return &pm, nil
}

func decodePaymentMethodRecord(record *endpointv1.PaymentMethodRecord) (*model.PaymentMethod, error) {
	if record == nil {
		return nil, merrors.InvalidArgument("payment_method_record is required")
	}
	pm, err := decodePaymentMethodProto(record.GetPaymentMethod())
	if err != nil {
		return nil, err
	}
	if err := applyPermissionBound(pm, record.GetPermissionBound()); err != nil {
		return nil, err
	}
	return pm, nil
}

func decodePaymentMethodProto(method *endpointv1.PaymentMethod) (*model.PaymentMethod, error) {
	if method == nil {
		return nil, merrors.InvalidArgument("payment_method is required")
	}

	recipientRef, err := parseRequiredObjectID(method.GetRecipientRef(), "payment_method.recipient_ref")
	if err != nil {
		return nil, err
	}
	pt, err := paymentTypeFromProto(method.GetType(), "payment_method.type")
	if err != nil {
		return nil, err
	}

	return &model.PaymentMethod{
		Describable:  describableFromProto(method.GetDescribable()),
		RecipientRef: recipientRef,
		Type:         pt,
		Data:         cloneBytes(method.GetData()),
		IsMain:       method.GetIsMain(),
	}, nil
}

func encodePaymentMethodProto(pm *model.PaymentMethod) (*endpointv1.PaymentMethod, error) {
	if pm == nil {
		return nil, merrors.InvalidArgument("payment method is required")
	}
	pt, err := paymentTypeToProto(pm.Type)
	if err != nil {
		return nil, err
	}
	return &endpointv1.PaymentMethod{
		Describable:  describableToProto(pm.Describable),
		RecipientRef: toObjectHex(pm.RecipientRef),
		Type:         pt,
		Data:         cloneBytes(pm.Data),
		IsMain:       pm.IsMain,
	}, nil
}

func encodePaymentMethodRecord(pm *model.PaymentMethod) (*endpointv1.PaymentMethodRecord, error) {
	method, err := encodePaymentMethodProto(pm)
	if err != nil {
		return nil, err
	}
	return &endpointv1.PaymentMethodRecord{
		PermissionBound: permissionBoundFromModel(pm),
		PaymentMethod:   method,
	}, nil
}

func decodePaymentMethods(items []*endpointv1.PaymentMethodRecord) ([]model.PaymentMethod, error) {
	if len(items) == 0 {
		return nil, nil
	}
	res := make([]model.PaymentMethod, 0, len(items))
	for i := range items {
		pm, err := decodePaymentMethodRecord(items[i])
		if err != nil {
			return nil, err
		}
		res = append(res, *pm)
	}
	return res, nil
}

func paymentTypeFromProto(value endpointv1.PaymentMethodType, field string) (model.PaymentType, error) {
	switch value {
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
		return model.PaymentTypeIban, nil
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
		return model.PaymentTypeCard, nil
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
		return model.PaymentTypeCardToken, nil
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
		return model.PaymentTypeBankAccount, nil
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
		return model.PaymentTypeWallet, nil
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
		return model.PaymentTypeCryptoAddress, nil
	case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
		return model.PaymentTypeLedger, nil
	default:
		return model.PaymentTypeIban, merrors.InvalidArgument(fmt.Sprintf("%s has unsupported value: %s", field, value.String()), field)
	}
}

func paymentTypeToProto(value model.PaymentType) (endpointv1.PaymentMethodType, error) {
	switch value {
	case model.PaymentTypeIban:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN, nil
	case model.PaymentTypeCard:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, nil
	case model.PaymentTypeCardToken:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, nil
	case model.PaymentTypeBankAccount:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT, nil
	case model.PaymentTypeWallet:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, nil
	case model.PaymentTypeCryptoAddress:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, nil
	case model.PaymentTypeLedger:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, nil
	default:
		return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported payment method type: %s", value.String()), "type")
	}
}

func describableFromProto(src *describablev1.Describable) model.Describable {
	if src == nil {
		return model.Describable{}
	}
	res := model.Describable{Name: src.GetName()}
	if src.Description != nil {
		v := src.GetDescription()
		res.Description = &v
	}
	return res
}

func describableToProto(src model.Describable) *describablev1.Describable {
	if strings.TrimSpace(src.Name) == "" && src.Description == nil {
		return nil
	}
	res := &describablev1.Describable{
		Name: src.Name,
	}
	if src.Description != nil {
		v := *src.Description
		res.Description = &v
	}
	return res
}

func cloneBytes(src []byte) []byte {
	if len(src) == 0 {
		return nil
	}
	dst := make([]byte, len(src))
	copy(dst, src)
	return dst
}

func permissionBoundFromModel(pm *model.PaymentMethod) *pboundv1.PermissionBound {
	if pm == nil {
		return nil
	}
	return &pboundv1.PermissionBound{
		Storable: &storablev1.Storable{
			Id:        toObjectHex(pm.ID),
			CreatedAt: toProtoTime(pm.CreatedAt),
			UpdatedAt: toProtoTime(pm.UpdatedAt),
		},
		Archivable: &archivablev1.Archivable{
			IsArchived: pm.Archived,
		},
		OrganizationBound: &oboundv1.OrganizationBound{
			OrganizationRef: toObjectHex(pm.GetOrganizationRef()),
		},
		PermissionRef: toObjectHex(pm.GetPermissionRef()),
	}
}

func applyPermissionBound(pm *model.PaymentMethod, src *pboundv1.PermissionBound) error {
	if pm == nil || src == nil {
		return nil
	}

	if storable := src.GetStorable(); storable != nil {
		if methodRef, err := parseOptionalObjectID(storable.GetId(), "payment_method_record.permission_bound.storable.id"); err != nil {
			return err
		} else if methodRef != bson.NilObjectID {
			pm.ID = methodRef
		}
		pm.CreatedAt = fromProtoTime(storable.GetCreatedAt())
		pm.UpdatedAt = fromProtoTime(storable.GetUpdatedAt())
	}

	if archivable := src.GetArchivable(); archivable != nil {
		pm.Archived = archivable.GetIsArchived()
	}

	if orgBound := src.GetOrganizationBound(); orgBound != nil {
		if orgRef, err := parseOptionalObjectID(orgBound.GetOrganizationRef(), "payment_method_record.permission_bound.organization_bound.organization_ref"); err != nil {
			return err
		} else if orgRef != bson.NilObjectID {
			pm.SetOrganizationRef(orgRef)
		}
	}

	if permissionRef, err := parseOptionalObjectID(src.GetPermissionRef(), "payment_method_record.permission_bound.permission_ref"); err != nil {
		return err
	} else if permissionRef != bson.NilObjectID {
		pm.SetPermissionRef(permissionRef)
	}

	return nil
}

func parseOptionalObjectID(value, field string) (bson.ObjectID, error) {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return bson.NilObjectID, nil
	}
	ref, err := bson.ObjectIDFromHex(trimmed)
	if err != nil {
		return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s must be a valid object id", field), field)
	}
	return ref, nil
}

func parseRequiredObjectID(value, field string) (bson.ObjectID, error) {
	ref, err := parseOptionalObjectID(value, field)
	if err != nil {
		return bson.NilObjectID, err
	}
	if ref == bson.NilObjectID {
		return bson.NilObjectID, merrors.InvalidArgument(field+" is required", field)
	}
	return ref, nil
}

func toObjectHex(value bson.ObjectID) string {
	if value == bson.NilObjectID {
		return ""
	}
	return value.Hex()
}

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

func fromProtoTime(value *timestamppb.Timestamp) time.Time {
	if value == nil {
		return time.Time{}
	}
	return value.AsTime()
}

func grpcErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
	statusErr, ok := status.FromError(err)
	if !ok {
		return response.Internal(logger, source, err)
	}

	switch statusErr.Code() {
	case codes.InvalidArgument:
		return response.BadRequest(logger, source, "invalid_argument", statusErr.Message())
	case codes.NotFound:
		return response.NotFound(logger, source, statusErr.Message())
	case codes.PermissionDenied:
		return response.AccessDenied(logger, source, statusErr.Message())
	case codes.Unauthenticated:
		return response.Unauthorized(logger, source, statusErr.Message())
	case codes.AlreadyExists, codes.Aborted:
		return response.DataConflict(logger, source, statusErr.Message())
	case codes.Unimplemented:
		return response.NotImplemented(logger, source, statusErr.Message())
	case codes.FailedPrecondition:
		return response.Error(logger, source, http.StatusPreconditionFailed, "failed_precondition", statusErr.Message())
	case codes.DeadlineExceeded:
		return response.Error(logger, source, http.StatusGatewayTimeout, "deadline_exceeded", statusErr.Message())
	case codes.Unavailable:
		return response.Error(logger, source, http.StatusServiceUnavailable, "service_unavailable", statusErr.Message())
	default:
		return response.Internal(logger, source, err)
	}
}
