package graph

import (
	"context"

	"github.com/alcionai/clues"
	"github.com/microsoftgraph/msgraph-sdk-go/models"

	"github.com/alcionai/corso/src/internal/common/ptr"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/path"
)

// Idable represents objects that implement msgraph-sdk-go/models.entityable
// and have the concept of an ID.
type Idable interface {
	GetId() *string
}

// Descendable represents objects that implement msgraph-sdk-go/models.entityable
// and have the concept of a "parent folder".
type Descendable interface {
	Idable
	GetParentFolderId() *string
}

// Displayable represents objects that implement msgraph-sdk-go/models.entityable
// and have the concept of a display name.
type Displayable interface {
	Idable
	GetDisplayName() *string
}

type Container interface {
	Descendable
	Displayable
}

// CachedContainer is used for local unit tests but also makes it so that this
// code can be broken into generic- and service-specific chunks later on to
// reuse logic in IDToPath.
type CachedContainer interface {
	Container
	// Location contains either the display names for the dirs (if this is a calendar)
	// or nil
	Location() *path.Builder
	SetLocation(*path.Builder)
	// Path contains either the ids for the dirs (if this is a calendar)
	// or the display names for the dirs
	Path() *path.Builder
	SetPath(*path.Builder)
}

// ContainerResolver houses functions for getting information about containers
// from remote APIs (i.e. resolve folder paths with Graph API). Resolvers may
// cache information about containers.
type ContainerResolver interface {
	// IDToPath takes an m365 container ID and converts it to a hierarchical path
	// to that container. The path has a similar format to paths on the local
	// file system.
	IDToPath(ctx context.Context, m365ID string) (*path.Builder, *path.Builder, error)

	// Populate performs initialization steps for the resolver
	// @param ctx is necessary param for Graph API tracing
	// @param baseFolderID represents the M365ID base that the resolver will
	// conclude its search. Default input is "".
	Populate(ctx context.Context, errs *fault.Bus, baseFolderID string, baseContainerPath ...string) error

	// PathInCache performs a look up of a path representation
	// and returns the m365ID of directory iff the pathString
	// matches the path of a container within the cache.
	// @returns bool represents if m365ID was found.
	PathInCache(pathString string) (string, bool)
	// LocationInCache performs a look up of a path representation
	// and returns the m365ID of directory iff the pathString
	// matches the logical path of a container within the cache.
	// @returns bool represents if m365ID was found.
	LocationInCache(pathString string) (string, bool)

	AddToCache(ctx context.Context, m365Container Container) error

	// Items returns the containers in the cache.
	Items() []CachedContainer
}

// ======================================
// cachedContainer Implementations
// ======================================

var _ CachedContainer = &CacheFolder{}

type CacheFolder struct {
	Container
	l *path.Builder
	p *path.Builder
}

// NewCacheFolder public constructor for struct
func NewCacheFolder(c Container, pb, lpb *path.Builder) CacheFolder {
	cf := CacheFolder{
		Container: c,
		l:         lpb,
		p:         pb,
	}

	return cf
}

// =========================================
// Required Functions to satisfy interfaces
// =========================================

func (cf CacheFolder) Location() *path.Builder {
	return cf.l
}

func (cf *CacheFolder) SetLocation(newLocation *path.Builder) {
	cf.l = newLocation
}

func (cf CacheFolder) Path() *path.Builder {
	return cf.p
}

func (cf *CacheFolder) SetPath(newPath *path.Builder) {
	cf.p = newPath
}

// CalendarDisplayable is a transformative struct that aligns
// models.Calendarable interface with the container interface.
// Calendars do not have the 2 of the
type CalendarDisplayable struct {
	models.Calendarable
	parentID string
}

// GetDisplayName returns the *string of the calendar name
func (c CalendarDisplayable) GetDisplayName() *string {
	return c.GetName()
}

// GetParentFolderId returns the default calendar name address
// EventCalendars have a flat hierarchy and Calendars are rooted
// at the default
//
//nolint:revive
func (c CalendarDisplayable) GetParentFolderId() *string {
	return &c.parentID
}

// CreateCalendarDisplayable helper function to create the
// calendarDisplayable during msgraph-sdk-go iterative process
// @param entry is the input supplied by pageIterator.Iterate()
// @param parentID of Calendar sets. Only populate when used with
// EventCalendarCache
func CreateCalendarDisplayable(entry any, parentID string) *CalendarDisplayable {
	calendar, ok := entry.(models.Calendarable)
	if !ok {
		return nil
	}

	return &CalendarDisplayable{
		Calendarable: calendar,
		parentID:     parentID,
	}
}

// =========================================
// helper funcs
// =========================================

// CheckIDAndName is a validator that ensures the ID
// and name are populated and not zero valued.
func CheckIDAndName(c Container) error {
	if c == nil {
		return clues.New("nil container")
	}

	id := ptr.Val(c.GetId())
	if len(id) == 0 {
		return clues.New("container missing ID")
	}

	dn := ptr.Val(c.GetDisplayName())
	if len(dn) == 0 {
		return clues.New("container missing display name").With("container_id", id)
	}

	return nil
}

// CheckIDNameAndParentFolderID is a validator that ensures the ID
// and name are populated and not zero valued.
func CheckIDNameAndParentFolderID(c Container) error {
	err := CheckIDAndName(c)
	if err != nil {
		return err
	}

	pfid := ptr.Val(c.GetParentFolderId())
	if len(pfid) == 0 {
		return clues.New("container missing parent folder id").With("container_id", ptr.Val(c.GetId()))
	}

	return nil
}
