package datastore

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
)

// Content represents a row in the contents table (storage blob metadata).
type Content struct {
	InodeID                string
	StorageType            StorageType
	ContentLayout          ContentLayout
	StorageRef             string
	StorageRefHash         string
	StorageEncryptionMode  StorageEncryptionMode
	StorageEncryptionKeyID string
	ContentBlob            []byte
	ContentType            string
	ChecksumSHA256         string
	SourceID               string
}

// InsertContent inserts a content row.
func (s *Store) InsertContent(ctx context.Context, content *Content) error {
	mode := fileStorageEncryptionModeForWrite(content.StorageEncryptionMode)
	_, err := s.db.ExecContext(ctx, `INSERT INTO contents
		(`+s.scope.InsCols(`inode_id, storage_type, content_layout, storage_ref, storage_ref_hash, storage_encryption_mode, storage_encryption_key_id,
		 content_blob, content_type, checksum_sha256, source_id`)+`)
		VALUES (`+s.scope.InsVals(`?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?`)+`)`,
		s.scope.Args(content.InodeID, content.StorageType, contentLayoutForWrite(content.ContentLayout), content.StorageRef, StorageRefHash(content.StorageRef), mode,
			storageEncryptionKeyIDForWrite(mode, content.StorageEncryptionKeyID),
			nilBytes(content.ContentBlob), nullStr(content.ContentType),
			nullStr(content.ChecksumSHA256), nullStr(content.SourceID))...)
	return err
}

// InsertContentTx inserts a content row inside an existing transaction.
func (s *Store) InsertContentTx(db execer, content *Content) error {
	mode := fileStorageEncryptionModeForWrite(content.StorageEncryptionMode)
	_, err := db.Exec(`INSERT INTO contents
		(`+s.scope.InsCols(`inode_id, storage_type, content_layout, storage_ref, storage_ref_hash, storage_encryption_mode, storage_encryption_key_id,
		 content_blob, content_type, checksum_sha256, source_id`)+`)
		VALUES (`+s.scope.InsVals(`?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?`)+`)`,
		s.scope.Args(content.InodeID, content.StorageType, contentLayoutForWrite(content.ContentLayout), content.StorageRef, StorageRefHash(content.StorageRef), mode,
			storageEncryptionKeyIDForWrite(mode, content.StorageEncryptionKeyID),
			nilBytes(content.ContentBlob), nullStr(content.ContentType),
			nullStr(content.ChecksumSHA256), nullStr(content.SourceID))...)
	return err
}

// GetContent retrieves a content row by inode ID.
func (s *Store) GetContent(ctx context.Context, inodeID string) (*Content, error) {
	row := s.db.QueryRowContext(ctx, `SELECT inode_id, storage_type, content_layout, storage_ref,
		storage_ref_hash, storage_encryption_mode, storage_encryption_key_id,
		content_blob, content_type, checksum_sha256, source_id
		FROM contents WHERE `+s.scope.And(`inode_id = ?`), s.scope.Args(inodeID)...)
	return scanContent(row)
}

// UpdateContentTx updates generic single-layout content metadata inside a
// transaction. Append-log metadata has a dedicated CAS-only write path and
// must never be converted by this helper.
func (s *Store) UpdateContentTx(db execer, inodeID string, storageType StorageType, storageRef, contentType, checksum string, contentBlob []byte, storageEncryptionMode StorageEncryptionMode) error {
	mode := fileStorageEncryptionModeForWrite(storageEncryptionMode)
	res, err := db.Exec(`UPDATE contents SET
		storage_type = ?, storage_ref = ?, storage_ref_hash = ?, storage_encryption_mode = ?,
		content_blob = ?, content_type = ?, checksum_sha256 = ?
		WHERE `+s.scope.And(`inode_id = ? AND content_layout = ?`),
		append([]any{storageType, storageRef, StorageRefHash(storageRef), mode,
			nilBytes(contentBlob), nullStr(contentType), nullStr(checksum)}, s.scope.Args(inodeID, ContentLayoutSingle)...)...)
	if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows == 0 {
		var layout ContentLayout
		err := db.QueryRow(`SELECT content_layout FROM contents WHERE `+s.scope.And(`inode_id = ?`), s.scope.Args(inodeID)...).Scan(&layout)
		if errors.Is(err, sql.ErrNoRows) {
			return ErrNotFound
		}
		if err != nil {
			return fmt.Errorf("read content layout after generic update: %w", err)
		}
		if layout != ContentLayoutSingle {
			return ErrContentLayoutConflict
		}
	}
	return nil
}

func scanContent(row *sql.Row) (*Content, error) {
	var c Content
	var contentType, checksum, sourceID sql.NullString
	var encryptionMode string
	err := row.Scan(&c.InodeID, &c.StorageType, &c.ContentLayout, &c.StorageRef,
		&c.StorageRefHash, &encryptionMode, &c.StorageEncryptionKeyID,
		&c.ContentBlob, &contentType, &checksum, &sourceID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, fmt.Errorf("scan content: %w", err)
	}
	c.StorageEncryptionMode = StorageEncryptionMode(encryptionMode)
	if c.StorageRefHash == "" {
		c.StorageRefHash = StorageRefHash(c.StorageRef)
	}
	c.ContentType = contentType.String
	c.ChecksumSHA256 = checksum.String
	c.SourceID = sourceID.String
	return &c, nil
}
