package backend

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync"

	"github.com/emersion/go-ical"
	"github.com/emersion/go-webdav"
	"github.com/emersion/go-webdav/caldav"
	"nxcaldav/internal/config"
)

type MemBackend struct {
	sync.RWMutex
	allCalendars   map[string]*caldav.Calendar
	objects        map[string]map[string]*caldav.CalendarObject
	userCalendars  map[string][]string
	calendarOwner  map[string]string
	calendarAccess map[string]map[string]string
}

func NewMemBackend(cfg *config.Config) *MemBackend {
	b := &MemBackend{
		allCalendars:   make(map[string]*caldav.Calendar),
		objects:        make(map[string]map[string]*caldav.CalendarObject),
		userCalendars:  make(map[string][]string),
		calendarOwner:  make(map[string]string),
		calendarAccess: make(map[string]map[string]string),
	}

	groupMembers := make(map[string][]string)
	for _, u := range cfg.Users {
		for _, g := range u.Groups {
			groupMembers[g] = append(groupMembers[g], u.Name)
		}
	}

	for _, c := range cfg.Calendars {
		path := fmt.Sprintf("/%s/calendars/%s/", c.Owner, c.ID)
		cal := &caldav.Calendar{
			Path:                  path,
			Name:                  c.ID,
			SupportedComponentSet: []string{"VEVENT", "VTODO"},
		}
		b.allCalendars[path] = cal
		b.calendarOwner[path] = c.Owner
		b.userCalendars[c.Owner] = append(b.userCalendars[c.Owner], path)

		accessMap := make(map[string]string)
		for _, a := range c.Access {
			var tUsers []string
			if a.User != "" {
				tUsers = append(tUsers, a.User)
			}
			if a.Group != "" {
				tUsers = append(tUsers, groupMembers[a.Group]...)
			}
			if a.Groups != "" {
				tUsers = append(tUsers, groupMembers[a.Groups]...)
			}
			for _, u := range tUsers {
				if _, exists := accessMap[u]; !exists {
					b.userCalendars[u] = append(b.userCalendars[u], path)
				}
				accessMap[u] = a.Mode
			}
		}
		b.calendarAccess[path] = accessMap
	}

	return b
}

func (b *MemBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
	principal, ok := ctx.Value("principal").(string)
	if !ok {
		return "", errors.New("no principal in context")
	}
	return principal, nil
}

func (b *MemBackend) getUsername(ctx context.Context) (string, error) {
	principal, err := b.CurrentUserPrincipal(ctx)
	if err != nil {
		return "", err
	}
	return strings.Trim(principal, "/"), nil
}

func (b *MemBackend) checkAccess(ctx context.Context, calendarPath string, requiredMode string) error {
	username, err := b.getUsername(ctx)
	if err != nil {
		return err
	}

	if !strings.HasSuffix(calendarPath, "/") {
		calendarPath += "/"
	}

	owner := b.calendarOwner[calendarPath]
	if owner == username {
		return nil
	}

	mode, ok := b.calendarAccess[calendarPath][username]
	if !ok {
		return webdav.NewHTTPError(http.StatusForbidden, errors.New("access denied"))
	}

	if requiredMode == "write" && mode != "read-write" {
		return webdav.NewHTTPError(http.StatusForbidden, errors.New("read-only access"))
	}

	return nil
}

func (b *MemBackend) filterCalendar(ctx context.Context, calendarPath string, original *ical.Calendar) (*ical.Calendar, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return nil, err
	}

	if !strings.HasSuffix(calendarPath, "/") {
		calendarPath += "/"
	}

	owner := b.calendarOwner[calendarPath]
	if username == owner {
		return original, nil
	}

	filtered := ical.NewCalendar()
	filtered.Props = original.Props

	for _, child := range original.Children {
		if child.Name != "VEVENT" && child.Name != "VTODO" {
			filtered.Children = append(filtered.Children, child)
			continue
		}

		class := "CONFIDENTIAL"
		if prop := child.Props.Get("CLASS"); prop != nil {
			class = strings.ToUpper(prop.Value)
		}

		switch class {
		case "PRIVATE":
			continue
		case "CONFIDENTIAL":
			redacted := ical.NewComponent(child.Name)
			propsToKeep := []string{
				"UID", "DTSTAMP", "DTSTART", "DTEND", "DURATION", "CLASS",
				"DUE", "COMPLETED", "STATUS", "PRIORITY", "PERCENT-COMPLETE",
			}
			for _, pName := range propsToKeep {
				if props, ok := child.Props[pName]; ok {
					redacted.Props[pName] = props
				}
			}
			redacted.Props.SetText("SUMMARY", "Busy")
			filtered.Children = append(filtered.Children, redacted)
		default:
			filtered.Children = append(filtered.Children, child)
		}
	}

	if len(filtered.Children) == 0 {
		return nil, nil
	}

	return filtered, nil
}

// CalDAV Backend implementation

func (b *MemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("/%s/calendars/", username), nil
}

func (b *MemBackend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error) {
	username, err := b.getUsername(ctx)
	if err != nil {
		return nil, err
	}

	b.RLock()
	defer b.RUnlock()

	paths := b.userCalendars[username]
	res := make([]caldav.Calendar, 0, len(paths))
	for _, p := range paths {
		if cal, ok := b.allCalendars[p]; ok {
			res = append(res, *cal)
		}
	}
	return res, nil
}

func (b *MemBackend) GetCalendar(ctx context.Context, path string) (*caldav.Calendar, error) {
	b.RLock()
	defer b.RUnlock()

	if !strings.HasSuffix(path, "/") {
		path += "/"
	}

	if cal, ok := b.allCalendars[path]; ok {
		return cal, nil
	}
	return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar not found"))
}

func (b *MemBackend) CreateCalendar(ctx context.Context, calendar *caldav.Calendar) error {
	return webdav.NewHTTPError(http.StatusForbidden, errors.New("calendar creation only via config"))
}

func (b *MemBackend) ListCalendarObjects(ctx context.Context, path string, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) {
	if err := b.checkAccess(ctx, path, "read"); err != nil {
		return nil, err
	}

	b.RLock()
	defer b.RUnlock()

	if !strings.HasSuffix(path, "/") {
		path += "/"
	}

	objs, ok := b.objects[path]
	if !ok {
		return []caldav.CalendarObject{}, nil
	}

	res := make([]caldav.CalendarObject, 0, len(objs))
	for _, obj := range objs {
		filteredData, err := b.filterCalendar(ctx, path, obj.Data)
		if err != nil {
			return nil, err
		}
		if filteredData == nil {
			continue
		}
		newObj := *obj
		newObj.Data = filteredData
		res = append(res, newObj)
	}
	return res, nil
}

func (b *MemBackend) GetCalendarObject(ctx context.Context, path string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
	lastSlash := strings.LastIndex(path, "/")
	if lastSlash == -1 {
		return nil, webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
	}
	parentPath := path[:lastSlash+1]

	if err := b.checkAccess(ctx, parentPath, "read"); err != nil {
		return nil, err
	}

	b.RLock()
	defer b.RUnlock()

	if objs, ok := b.objects[parentPath]; ok {
		if obj, ok := objs[path]; ok {
			filteredData, err := b.filterCalendar(ctx, parentPath, obj.Data)
			if err != nil {
				return nil, err
			}
			if filteredData == nil {
				return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
			}
			newObj := *obj
			newObj.Data = filteredData
			return &newObj, nil
		}
	}
	return nil, webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
}

func (b *MemBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (*caldav.CalendarObject, error) {
	lastSlash := strings.LastIndex(path, "/")
	if lastSlash == -1 {
		return nil, webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
	}
	parentPath := path[:lastSlash+1]

	if err := b.checkAccess(ctx, parentPath, "write"); err != nil {
		return nil, err
	}

	b.Lock()
	defer b.Unlock()

	obj := &caldav.CalendarObject{
		Path:     path,
		Data:     calendar,
		ETag:     fmt.Sprintf(`"%d"`, len(calendar.Events())),
	}

	if _, ok := b.objects[parentPath]; !ok {
		b.objects[parentPath] = make(map[string]*caldav.CalendarObject)
	}
	b.objects[parentPath][path] = obj
	return obj, nil
}

func (b *MemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
	lastSlash := strings.LastIndex(path, "/")
	if lastSlash == -1 {
		return webdav.NewHTTPError(http.StatusBadRequest, errors.New("invalid object path"))
	}
	parentPath := path[:lastSlash+1]

	if err := b.checkAccess(ctx, parentPath, "write"); err != nil {
		return err
	}

	b.Lock()
	defer b.Unlock()

	if objs, ok := b.objects[parentPath]; ok {
		delete(objs, path)
		return nil
	}
	return webdav.NewHTTPError(http.StatusNotFound, errors.New("calendar object not found"))
}

func (b *MemBackend) QueryCalendarObjects(ctx context.Context, path string, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) {
	return b.ListCalendarObjects(ctx, path, nil)
}
