// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/repo/tag/tag.go
package tag

import (
	"context"
	"fmt"

	"github.com/gocql/gocql"

	dom_crypto "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/crypto"
	dom_tag "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/tag"
)

type TagRepository struct {
	session *gocql.Session
}

func NewTagRepository(session *gocql.Session) dom_tag.Repository {
	return &TagRepository{
		session: session,
	}
}

// Create inserts a new tag with encrypted data
func (r *TagRepository) Create(ctx context.Context, tag *dom_tag.Tag) error {
	// Extract encrypted tag key components
	var ciphertext, nonce []byte
	if tag.EncryptedTagKey != nil {
		ciphertext = tag.EncryptedTagKey.Ciphertext
		nonce = tag.EncryptedTagKey.Nonce
	}

	// Insert into tags_by_id
	queryByID := `INSERT INTO maplefile.tags_by_id (id, user_id, encrypted_name, encrypted_color, encrypted_tag_key_ciphertext, encrypted_tag_key_nonce, created_at, modified_at, version, state) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
	if err := r.session.Query(queryByID,
		tag.ID, tag.UserID, tag.EncryptedName, tag.EncryptedColor, ciphertext, nonce, tag.CreatedAt, tag.ModifiedAt, tag.Version, tag.State).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to insert into tags_by_id: %w", err)
	}

	// Insert into tags_by_user
	queryByUser := `INSERT INTO maplefile.tags_by_user (user_id, id, encrypted_name, encrypted_color, encrypted_tag_key_ciphertext, encrypted_tag_key_nonce, created_at, modified_at, version, state) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
	if err := r.session.Query(queryByUser,
		tag.UserID, tag.ID, tag.EncryptedName, tag.EncryptedColor, ciphertext, nonce, tag.CreatedAt, tag.ModifiedAt, tag.Version, tag.State).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to insert into tags_by_user: %w", err)
	}

	return nil
}

// GetByID retrieves a tag by its ID with encrypted data
func (r *TagRepository) GetByID(ctx context.Context, id gocql.UUID) (*dom_tag.Tag, error) {
	query := `SELECT id, user_id, encrypted_name, encrypted_color, encrypted_tag_key_ciphertext, encrypted_tag_key_nonce, created_at, modified_at, version, state FROM maplefile.tags_by_id WHERE id = ? LIMIT 1`

	tag := &dom_tag.Tag{}
	var ciphertext, nonce []byte

	if err := r.session.Query(query, id).WithContext(ctx).Scan(
		&tag.ID, &tag.UserID, &tag.EncryptedName, &tag.EncryptedColor, &ciphertext, &nonce, &tag.CreatedAt, &tag.ModifiedAt, &tag.Version, &tag.State,
	); err != nil {
		if err == gocql.ErrNotFound {
			return nil, fmt.Errorf("tag not found")
		}
		return nil, fmt.Errorf("failed to get tag: %w", err)
	}

	// Reconstruct EncryptedTagKey from components
	if len(ciphertext) > 0 && len(nonce) > 0 {
		tag.EncryptedTagKey = &dom_crypto.EncryptedTagKey{
			Ciphertext: ciphertext,
			Nonce:      nonce,
			KeyVersion: 1,
		}
	}

	return tag, nil
}

// ListByUser retrieves all tags for a user with encrypted data
func (r *TagRepository) ListByUser(ctx context.Context, userID gocql.UUID) ([]*dom_tag.Tag, error) {
	query := `SELECT id, user_id, encrypted_name, encrypted_color, encrypted_tag_key_ciphertext, encrypted_tag_key_nonce, created_at, modified_at, version, state FROM maplefile.tags_by_user WHERE user_id = ?`

	iter := r.session.Query(query, userID).WithContext(ctx).Iter()
	defer iter.Close()

	var tags []*dom_tag.Tag
	tag := &dom_tag.Tag{}
	var ciphertext, nonce []byte

	for iter.Scan(&tag.ID, &tag.UserID, &tag.EncryptedName, &tag.EncryptedColor, &ciphertext, &nonce, &tag.CreatedAt, &tag.ModifiedAt, &tag.Version, &tag.State) {
		// Reconstruct EncryptedTagKey from components
		if len(ciphertext) > 0 && len(nonce) > 0 {
			tag.EncryptedTagKey = &dom_crypto.EncryptedTagKey{
				Ciphertext: ciphertext,
				Nonce:      nonce,
				KeyVersion: 1,
			}
		}

		tags = append(tags, tag)
		tag = &dom_tag.Tag{}
		ciphertext, nonce = nil, nil
	}

	if err := iter.Close(); err != nil {
		return nil, fmt.Errorf("failed to list tags: %w", err)
	}

	return tags, nil
}

// Update updates a tag with encrypted data
func (r *TagRepository) Update(ctx context.Context, tag *dom_tag.Tag) error {
	// Extract encrypted tag key components
	var ciphertext, nonce []byte
	if tag.EncryptedTagKey != nil {
		ciphertext = tag.EncryptedTagKey.Ciphertext
		nonce = tag.EncryptedTagKey.Nonce
	}

	// Update tags_by_id
	queryByID := `UPDATE maplefile.tags_by_id SET encrypted_name = ?, encrypted_color = ?, encrypted_tag_key_ciphertext = ?, encrypted_tag_key_nonce = ?, modified_at = ?, version = ?, state = ? WHERE id = ?`
	if err := r.session.Query(queryByID,
		tag.EncryptedName, tag.EncryptedColor, ciphertext, nonce, tag.ModifiedAt, tag.Version, tag.State, tag.ID).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to update tags_by_id: %w", err)
	}

	// Update tags_by_user
	queryByUser := `UPDATE maplefile.tags_by_user SET encrypted_name = ?, encrypted_color = ?, encrypted_tag_key_ciphertext = ?, encrypted_tag_key_nonce = ?, modified_at = ?, version = ?, state = ? WHERE user_id = ? AND id = ?`
	if err := r.session.Query(queryByUser,
		tag.EncryptedName, tag.EncryptedColor, ciphertext, nonce, tag.ModifiedAt, tag.Version, tag.State, tag.UserID, tag.ID).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to update tags_by_user: %w", err)
	}

	return nil
}

// DeleteByID deletes a tag by ID for a specific user
func (r *TagRepository) DeleteByID(ctx context.Context, userID, id gocql.UUID) error {
	// Delete from tags_by_id table
	queryByID := `DELETE FROM maplefile.tags_by_id WHERE id = ?`
	if err := r.session.Query(queryByID, id).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to delete from tags_by_id: %w", err)
	}

	// Delete from tags_by_user table
	queryByUser := `DELETE FROM maplefile.tags_by_user WHERE user_id = ? AND id = ?`
	if err := r.session.Query(queryByUser, userID, id).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to delete from tags_by_user: %w", err)
	}

	return nil
}

// AssignTag creates a tag assignment
// Note: This only updates tag_assignments_by_entity. The denormalized tables
// (collections_by_tag_id, files_by_tag_id) are maintained by their respective repositories.
func (r *TagRepository) AssignTag(ctx context.Context, assignment *dom_tag.TagAssignment) error {
	// Insert into tag_assignments_by_entity
	queryByEntity := `INSERT INTO maplefile.tag_assignments_by_entity (entity_id, entity_type, tag_id, user_id, created_at) VALUES (?, ?, ?, ?, ?)`
	if err := r.session.Query(queryByEntity,
		assignment.EntityID, assignment.EntityType, assignment.TagID, assignment.UserID, assignment.CreatedAt).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to insert into tag_assignments_by_entity: %w", err)
	}

	return nil
}

// UnassignTag removes a tag assignment
// Note: This only updates tag_assignments_by_entity. The denormalized tables
// (collections_by_tag_id, files_by_tag_id) are maintained by their respective repositories.
func (r *TagRepository) UnassignTag(ctx context.Context, tagID, entityID gocql.UUID, entityType string) error {
	// Delete from tag_assignments_by_entity
	queryByEntity := `DELETE FROM maplefile.tag_assignments_by_entity WHERE entity_id = ? AND entity_type = ? AND tag_id = ?`
	if err := r.session.Query(queryByEntity, entityID, entityType, tagID).WithContext(ctx).Exec(); err != nil {
		return fmt.Errorf("failed to delete from tag_assignments_by_entity: %w", err)
	}

	return nil
}

// GetTagsForEntity retrieves all tags assigned to an entity
func (r *TagRepository) GetTagsForEntity(ctx context.Context, entityID gocql.UUID, entityType string) ([]*dom_tag.Tag, error) {
	// First get tag IDs from assignments
	query := `SELECT tag_id FROM maplefile.tag_assignments_by_entity WHERE entity_id = ? AND entity_type = ?`
	iter := r.session.Query(query, entityID, entityType).WithContext(ctx).Iter()
	defer iter.Close()

	var tagIDs []gocql.UUID
	var tagID gocql.UUID

	for iter.Scan(&tagID) {
		tagIDs = append(tagIDs, tagID)
	}

	if err := iter.Close(); err != nil {
		return nil, fmt.Errorf("failed to get tag assignments: %w", err)
	}

	// Fetch tag details
	var tags []*dom_tag.Tag
	for _, tid := range tagIDs {
		tag, err := r.GetByID(ctx, tid)
		if err != nil {
			continue // Skip if tag not found
		}
		tags = append(tags, tag)
	}

	return tags, nil
}

// GetEntitiesWithTag retrieves all entity IDs that have a specific tag
// Uses denormalized tables (collections_by_tag_id, files_by_tag_id) for efficient queries
func (r *TagRepository) GetEntitiesWithTag(ctx context.Context, tagID gocql.UUID, entityType string) ([]gocql.UUID, error) {
	var query string
	var columnName string

	switch entityType {
	case "collection":
		query = `SELECT collection_id FROM maplefile.collections_by_tag_id WHERE tag_id = ?`
		columnName = "collection_id"
	case "file":
		query = `SELECT file_id FROM maplefile.files_by_tag_id WHERE tag_id = ?`
		columnName = "file_id"
	default:
		return nil, fmt.Errorf("unsupported entity type: %s", entityType)
	}

	iter := r.session.Query(query, tagID).WithContext(ctx).Iter()
	defer iter.Close()

	var entityIDs []gocql.UUID
	var entityID gocql.UUID

	for iter.Scan(&entityID) {
		entityIDs = append(entityIDs, entityID)
	}

	if err := iter.Close(); err != nil {
		return nil, fmt.Errorf("failed to get entities with tag from %s: %w", columnName, err)
	}

	return entityIDs, nil
}

// GetAssignmentsByTag retrieves all assignments for a specific tag
// Queries both denormalized tables (collections_by_tag_id and files_by_tag_id)
func (r *TagRepository) GetAssignmentsByTag(ctx context.Context, tagID gocql.UUID) ([]*dom_tag.TagAssignment, error) {
	var assignments []*dom_tag.TagAssignment

	// Get collection assignments
	collectionQuery := `SELECT collection_id, owner_id, created_at FROM maplefile.collections_by_tag_id WHERE tag_id = ?`
	collectionIter := r.session.Query(collectionQuery, tagID).WithContext(ctx).Iter()

	var collectionID, ownerID gocql.UUID
	var createdAt interface{}

	for collectionIter.Scan(&collectionID, &ownerID, &createdAt) {
		assignments = append(assignments, &dom_tag.TagAssignment{
			TagID:      tagID,
			EntityID:   collectionID,
			EntityType: "collection",
			UserID:     ownerID,
		})
	}
	collectionIter.Close()

	// Get file assignments
	fileQuery := `SELECT file_id, owner_id, created_at FROM maplefile.files_by_tag_id WHERE tag_id = ?`
	fileIter := r.session.Query(fileQuery, tagID).WithContext(ctx).Iter()

	var fileID gocql.UUID

	for fileIter.Scan(&fileID, &ownerID, &createdAt) {
		assignments = append(assignments, &dom_tag.TagAssignment{
			TagID:      tagID,
			EntityID:   fileID,
			EntityType: "file",
			UserID:     ownerID,
		})
	}

	if err := fileIter.Close(); err != nil {
		return nil, fmt.Errorf("failed to get file assignments by tag: %w", err)
	}

	return assignments, nil
}

// GetAssignmentsByEntity retrieves all assignments for a specific entity
func (r *TagRepository) GetAssignmentsByEntity(ctx context.Context, entityID gocql.UUID, entityType string) ([]*dom_tag.TagAssignment, error) {
	query := `SELECT tag_id, user_id, created_at FROM maplefile.tag_assignments_by_entity WHERE entity_id = ? AND entity_type = ?`
	iter := r.session.Query(query, entityID, entityType).WithContext(ctx).Iter()
	defer iter.Close()

	var assignments []*dom_tag.TagAssignment
	var tagID, userID gocql.UUID
	var createdAt interface{}

	for iter.Scan(&tagID, &userID, &createdAt) {
		assignment := &dom_tag.TagAssignment{
			TagID:      tagID,
			EntityID:   entityID,
			EntityType: entityType,
			UserID:     userID,
		}
		assignments = append(assignments, assignment)
	}

	if err := iter.Close(); err != nil {
		return nil, fmt.Errorf("failed to get assignments by entity: %w", err)
	}

	return assignments, nil
}
