package models

import (
	"context"
	"database/sql"
	"errors"
	"log"
	"screenmark/screenmark/.gen/haystack/haystack/model"
	. "screenmark/screenmark/.gen/haystack/haystack/table"

	. "github.com/go-jet/jet/v2/postgres"
	"github.com/go-jet/jet/v2/qrm"
	"github.com/google/uuid"
)

type UserModel struct {
	dbPool *sql.DB
}

type ImageWithProperties struct {
	ID uuid.UUID

	Image model.Image

	Locations []model.Locations
	Events    []model.Events
	Notes     []model.Notes
	Contacts  []model.Contacts
}

type PropertiesWithImage struct {
	Locations []struct {
		model.Locations

		Images uuid.UUIDs
	}
	Contacts []struct {
		model.Contacts

		Images uuid.UUIDs
	}
	Events []struct {
		model.Events

		Images uuid.UUIDs
	}
	Notes []struct {
		model.Notes

		Images uuid.UUIDs
	}
}

func transpose(imageProperties []ImageWithProperties) PropertiesWithImage {
	// EntityID -> []ImageIDs
	dependencies := make(map[uuid.UUID]uuid.UUIDs)

	addDependency := func(entityId uuid.UUID, imageId uuid.UUID) {
		deps, exists := dependencies[entityId]
		if !exists {
			dep := uuid.UUIDs{imageId}
			dependencies[entityId] = dep

			return
		}

		dependencies[entityId] = append(deps, imageId)
	}

	contactMap := make(map[uuid.UUID]model.Contacts)
	locationMap := make(map[uuid.UUID]model.Locations)
	noteMap := make(map[uuid.UUID]model.Notes)
	eventMap := make(map[uuid.UUID]model.Events)

	for _, image := range imageProperties {
		for _, contact := range image.Contacts {
			contactMap[contact.ID] = contact
			addDependency(contact.ID, image.Image.ID)
		}
		for _, location := range image.Locations {
			locationMap[location.ID] = location
			addDependency(location.ID, image.Image.ID)
		}
		for _, note := range image.Notes {
			noteMap[note.ID] = note
			addDependency(note.ID, image.Image.ID)
		}
		for _, event := range image.Events {
			eventMap[event.ID] = event
			addDependency(event.ID, image.Image.ID)
		}
	}

	properties := PropertiesWithImage{
		Contacts: make([]struct {
			model.Contacts
			Images uuid.UUIDs
		}, 0),
		Locations: make([]struct {
			model.Locations
			Images uuid.UUIDs
		}, 0),
		Notes: make([]struct {
			model.Notes
			Images uuid.UUIDs
		}, 0),
		Events: make([]struct {
			model.Events
			Images uuid.UUIDs
		}, 0),
	}

	for contactId, contact := range contactMap {
		properties.Contacts = append(properties.Contacts, struct {
			model.Contacts
			Images uuid.UUIDs
		}{
			Contacts: contact,
			Images:   dependencies[contactId],
		})
	}

	for locationId, location := range locationMap {
		properties.Locations = append(properties.Locations, struct {
			model.Locations
			Images uuid.UUIDs
		}{
			Locations: location,
			Images:    dependencies[locationId],
		})
	}

	for noteId, note := range noteMap {
		properties.Notes = append(properties.Notes, struct {
			model.Notes
			Images uuid.UUIDs
		}{
			Notes:  note,
			Images: dependencies[noteId],
		})
	}

	for eventId, event := range eventMap {
		properties.Events = append(properties.Events, struct {
			model.Events
			Images uuid.UUIDs
		}{
			Events: event,
			Images: dependencies[eventId],
		})
	}

	return properties
}

type TypedProperties struct {
	Type string `json:"type"`
	Data any    `json:"data"`
}

func propertiesToTypeArray(properties PropertiesWithImage) []TypedProperties {
	typedProperties := make([]TypedProperties, 0)

	for _, location := range properties.Locations {
		typedProperties = append(typedProperties, TypedProperties{
			Type: "location",
			Data: location,
		})
	}

	for _, contact := range properties.Contacts {
		typedProperties = append(typedProperties, TypedProperties{
			Type: "contact",
			Data: contact,
		})
	}

	for _, note := range properties.Notes {
		typedProperties = append(typedProperties, TypedProperties{
			Type: "note",
			Data: note,
		})
	}

	for _, event := range properties.Events {
		typedProperties = append(typedProperties, TypedProperties{
			Type: "event",
			Data: event,
		})
	}

	return typedProperties
}

func GetTypedImageProperties(imageProperties []ImageWithProperties) []TypedProperties {
	return propertiesToTypeArray(transpose(imageProperties))
}

func getUserIdFromImage(ctx context.Context, dbPool *sql.DB, imageId uuid.UUID) (uuid.UUID, error) {
	getUserIdStmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))

	log.Println(getUserIdStmt.DebugSql())

	userImages := []model.UserImages{}
	err := getUserIdStmt.QueryContext(ctx, dbPool, &userImages)
	if err != nil {
		return uuid.Nil, err
	}

	if len(userImages) != 1 {
		return uuid.Nil, errors.New("Expected exactly one choice.")
	}

	return userImages[0].UserID, nil
}

func getListImagesStmt() SelectStatement {
	return SELECT(
		UserImages.ID.AS("ImageWithProperties.ID"),
		Image.ID,
		Image.ImageName,
		ImageTags.AllColumns,
		UserTags.AllColumns,
		ImageText.AllColumns,
		ImageLinks.AllColumns,
		ImageLocations.AllColumns,
		Locations.AllColumns,
		ImageEvents.AllColumns,
		Events.AllColumns,
		ImageContacts.AllColumns,
		Contacts.AllColumns,
		ImageNotes.AllColumns,
		Notes.AllColumns,
	).
		FROM(
			UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
				LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(Image.ID)).
				LEFT_JOIN(UserTags, UserTags.ID.EQ(ImageTags.TagID)).
				LEFT_JOIN(ImageText, ImageText.ImageID.EQ(Image.ID)).
				LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(Image.ID)).
				LEFT_JOIN(ImageLocations, ImageLocations.ImageID.EQ(UserImages.ImageID)).
				LEFT_JOIN(Locations, Locations.ID.EQ(ImageLocations.LocationID)).
				LEFT_JOIN(ImageEvents, ImageEvents.ImageID.EQ(UserImages.ImageID)).
				LEFT_JOIN(Events, Events.ID.EQ(ImageEvents.EventID)).
				LEFT_JOIN(ImageContacts, ImageContacts.ImageID.EQ(UserImages.ImageID)).
				LEFT_JOIN(Contacts, Contacts.ID.EQ(ImageContacts.ContactID)).
				LEFT_JOIN(ImageNotes, ImageNotes.ImageID.EQ(UserImages.ImageID)).
				LEFT_JOIN(Notes, Notes.ID.EQ(ImageNotes.NoteID)))
}

func (m UserModel) ListImageWithProperties(ctx context.Context, userId uuid.UUID, imageId uuid.UUID) (ImageWithProperties, error) {
	listImagePropertiesStmt := getListImagesStmt().
		WHERE(UserImages.ImageID.EQ(UUID(imageId)))

	image := ImageWithProperties{}
	err := listImagePropertiesStmt.QueryContext(ctx, m.dbPool, &image)

	return image, err
}

func (m UserModel) ListWithProperties(ctx context.Context, userId uuid.UUID) ([]ImageWithProperties, error) {
	listWithPropertiesStmt := getListImagesStmt().
		WHERE(UserImages.UserID.EQ(UUID(userId)))

	images := []ImageWithProperties{}
	err := listWithPropertiesStmt.QueryContext(ctx, m.dbPool, &images)

	return images, err
}

func (m UserModel) GetUserIdFromEmail(ctx context.Context, email string) (uuid.UUID, error) {
	getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))

	user := model.Users{}
	err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)

	return user.ID, err
}

func (m UserModel) DoesUserExist(ctx context.Context, email string) bool {
	getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))

	user := model.Users{}
	err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)

	return err != qrm.ErrNoRows
}

func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, error) {
	insertUserStmt := Users.INSERT(Users.Email).VALUES(user.Email).RETURNING(Users.AllColumns)

	insertedUser := model.Users{}
	err := insertUserStmt.QueryContext(ctx, m.dbPool, &insertedUser)

	return insertedUser, err
}

type UserImageWithImage struct {
	model.UserImages

	Image model.Image
}

func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
	getUserImagesStmt := SELECT(
		UserImages.AllColumns,
		Image.ID,
		Image.ImageName,
	).
		FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID))).
		WHERE(UserImages.UserID.EQ(UUID(userId)))

	userImages := []UserImageWithImage{}
	err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages)

	return userImages, err
}

func NewUserModel(db *sql.DB) UserModel {
	return UserModel{dbPool: db}
}
