package peer

import (
	"context"
	"fmt"

	"github.com/go-faster/errors"

	"github.com/gotd/td/tg"
)

// PromiseDecorator is a decorator of peer promise.
type PromiseDecorator = func(Promise) Promise

// ConstraintError is a peer resolve constraint error.
type ConstraintError struct {
	Expected string
	Got      tg.InputPeerClass
}

// Error implements error.
func (c *ConstraintError) Error() string {
	return fmt.Sprintf("expected %q, got %T", c.Expected, c.Got)
}

func tryUnpackConstraint(p tg.InputPeerClass, resolveErr error) (tg.InputPeerClass, error) {
	var constraintErr *ConstraintError
	if errors.As(resolveErr, &constraintErr) {
		return constraintErr.Got, nil
	}
	return p, resolveErr
}

// OnlyChannel returns Promise which returns error if resolved peer is not a channel.
func OnlyChannel(p Promise) Promise {
	return func(ctx context.Context) (tg.InputPeerClass, error) {
		resolved, err := tryUnpackConstraint(p(ctx))
		if err != nil {
			return nil, err
		}

		switch resolved.(type) {
		case *tg.InputPeerChannel, *tg.InputPeerChannelFromMessage:
			return resolved, nil
		default:
			return nil, &ConstraintError{
				Expected: "channel",
				Got:      resolved,
			}
		}
	}
}

// OnlyChat returns Promise which returns error if resolved peer is not a chat.
func OnlyChat(p Promise) Promise {
	return func(ctx context.Context) (tg.InputPeerClass, error) {
		resolved, err := tryUnpackConstraint(p(ctx))
		if err != nil {
			return nil, err
		}

		switch resolved.(type) {
		case *tg.InputPeerChat:
			return resolved, nil
		default:
			return nil, &ConstraintError{
				Expected: "chat",
				Got:      resolved,
			}
		}
	}
}

// OnlyUser returns Promise which returns error if resolved peer is not a user.
func OnlyUser(p Promise) Promise {
	return func(ctx context.Context) (tg.InputPeerClass, error) {
		resolved, err := tryUnpackConstraint(p(ctx))
		if err != nil {
			return nil, err
		}

		switch resolved.(type) {
		case *tg.InputPeerUser, *tg.InputPeerUserFromMessage, *tg.InputPeerSelf:
			return resolved, nil
		default:
			return nil, &ConstraintError{
				Expected: "user",
				Got:      resolved,
			}
		}
	}
}

// OnlyUserID returns Promise which returns error if resolved peer is not a user object with ID.
// Unlike OnlyUser, it returns error if resolved peer is tg.InputPeerSelf.
func OnlyUserID(p Promise) Promise {
	return func(ctx context.Context) (tg.InputPeerClass, error) {
		resolved, err := tryUnpackConstraint(p(ctx))
		if err != nil {
			return nil, err
		}

		switch resolved.(type) {
		case *tg.InputPeerUser, *tg.InputPeerUserFromMessage:
			return resolved, nil
		default:
			return nil, &ConstraintError{
				Expected: "userID",
				Got:      resolved,
			}
		}
	}
}
