package meta

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

	"github.com/pingcap/failpoint"
)

const (
	ExtractMediaTypeImage = "image"
	ExtractMediaTypeAudio = "audio"
	ExtractMediaTypeVideo = "video"
	ExtractMediaTypeText  = "text"
)

type TenantExtractConfig struct {
	TenantID     string
	MediaType    string
	Enabled      bool
	APIBase      string // "" when NULL
	APIKeyCipher []byte // nil when NULL
	Model        string // "" when NULL
	Prompt       string // "" when NULL
	Protocol     string // "" when NULL; defaults to openai at API/runtime boundaries
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

func ValidExtractMediaType(t string) bool {
	switch t {
	case ExtractMediaTypeImage, ExtractMediaTypeAudio, ExtractMediaTypeVideo, ExtractMediaTypeText:
		return true
	}
	return false
}

func (s *Store) UpsertTenantExtractConfig(ctx context.Context, cfg *TenantExtractConfig) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "upsert_tenant_extract_config", start, &err)
	return upsertTenantExtractConfig(ctx, s.db, cfg)
}

// UpsertTenantExtractConfigTx is UpsertTenantExtractConfig inside an
// enclosing transaction (read-modify-write with a preceding
// GetTenantExtractConfigForUpdateTx).
func (s *Store) UpsertTenantExtractConfigTx(ctx context.Context, tx *sql.Tx, cfg *TenantExtractConfig) error {
	return upsertTenantExtractConfig(ctx, tx, cfg)
}

type execContexter interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

func upsertTenantExtractConfig(ctx context.Context, ex execContexter, cfg *TenantExtractConfig) error {
	if cfg == nil {
		return fmt.Errorf("nil tenant extract config")
	}
	if cfg.TenantID == "" {
		return fmt.Errorf("tenant extract config requires tenant id")
	}
	if !ValidExtractMediaType(cfg.MediaType) {
		return fmt.Errorf("invalid extract media type %q", cfg.MediaType)
	}
	now := time.Now().UTC()
	createdAt := cfg.CreatedAt.UTC()
	if cfg.CreatedAt.IsZero() {
		createdAt = now
	}
	updatedAt := cfg.UpdatedAt.UTC()
	if cfg.UpdatedAt.IsZero() {
		updatedAt = now
	}
	_, err := ex.ExecContext(ctx, `INSERT INTO tenant_extract_configs
		(tenant_id, media_type, enabled, api_base, api_key_cipher, model, prompt, protocol, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			enabled = VALUES(enabled),
			api_base = VALUES(api_base),
			api_key_cipher = VALUES(api_key_cipher),
			model = VALUES(model),
			prompt = VALUES(prompt),
			protocol = VALUES(protocol),
			updated_at = VALUES(updated_at)`,
		cfg.TenantID, cfg.MediaType, cfg.Enabled, nullStr(cfg.APIBase), nullableBytes(cfg.APIKeyCipher),
		nullStr(cfg.Model), nullStr(cfg.Prompt), nullStr(cfg.Protocol), createdAt, updatedAt)
	return err
}

const tenantExtractConfigColumns = `tenant_id, media_type, enabled, api_base, api_key_cipher, model, prompt,
		protocol, created_at, updated_at`

func scanTenantExtractConfig(row *sql.Row) (*TenantExtractConfig, error) {
	var rec TenantExtractConfig
	var apiBase sql.NullString
	var apiKeyCipher []byte
	var model sql.NullString
	var prompt sql.NullString
	var protocol sql.NullString
	if err := row.Scan(&rec.TenantID, &rec.MediaType, &rec.Enabled,
		&apiBase, &apiKeyCipher, &model, &prompt, &protocol, &rec.CreatedAt, &rec.UpdatedAt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	if apiBase.Valid {
		rec.APIBase = apiBase.String
	}
	if model.Valid {
		rec.Model = model.String
	}
	if prompt.Valid {
		rec.Prompt = prompt.String
	}
	if protocol.Valid {
		rec.Protocol = protocol.String
	}
	rec.APIKeyCipher = apiKeyCipher
	return &rec, nil
}

func (s *Store) GetTenantExtractConfig(ctx context.Context, tenantID, mediaType string) (out *TenantExtractConfig, err error) {
	start := time.Now()
	defer observeMeta(ctx, "get_tenant_extract_config", start, &err)
	row := s.db.QueryRowContext(ctx, `SELECT `+tenantExtractConfigColumns+`
		FROM tenant_extract_configs WHERE tenant_id = ? AND media_type = ?`, tenantID, mediaType)
	return scanTenantExtractConfig(row)
}

// GetTenantExtractConfigForUpdateTx reads the config row inside tx with
// SELECT ... FOR UPDATE so read-modify-write cycles serialize on the
// (tenant_id, media_type) row.
func (s *Store) GetTenantExtractConfigForUpdateTx(ctx context.Context, tx *sql.Tx, tenantID, mediaType string) (*TenantExtractConfig, error) {
	row := tx.QueryRowContext(ctx, `SELECT `+tenantExtractConfigColumns+`
		FROM tenant_extract_configs WHERE tenant_id = ? AND media_type = ? FOR UPDATE`, tenantID, mediaType)
	return scanTenantExtractConfig(row)
}

// LockTenantForConfigUpdateTx FOR UPDATEs the tenant row inside tx before any
// tenant_extract_configs read-modify-write, so a concurrent terminal delete
// (tenant row lock, then config row delete) cannot commit between the
// caller's authorization precheck and the config upsert and leave an
// encrypted provider key behind for a deleted tenant. The lock order is
// always tenant row, then config row. Missing tenants and tenants already
// deleting/deleted fail closed with ErrNotFound, matching admin
// anti-enumeration (a tenant on its way out is indistinguishable from gone).
func LockTenantForConfigUpdateTx(ctx context.Context, tx *sql.Tx, tenantID string) error {
	failpoint.InjectCall("beforeTenantExtractConfigLock", tenantID)
	var status string
	if err := tx.QueryRowContext(ctx, `SELECT status FROM tenants WHERE id = ? FOR UPDATE`, tenantID).Scan(&status); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return ErrNotFound
		}
		return err
	}
	switch TenantStatus(status) {
	case TenantDeleting, TenantDeleted:
		return ErrNotFound
	}
	return nil
}

func (s *Store) DeleteTenantExtractConfigs(ctx context.Context, tenantID string) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "delete_tenant_extract_configs", start, &err)
	_, err = s.db.ExecContext(ctx, `DELETE FROM tenant_extract_configs WHERE tenant_id = ?`, tenantID)
	return err
}

// deleteTenantExtractConfigsTx removes a tenant's extract configs inside an
// enclosing tenant-deletion transaction so encrypted provider API keys never
// outlive the tenant row.
func deleteTenantExtractConfigsTx(ctx context.Context, tx *sql.Tx, tenantID string) error {
	_, err := tx.ExecContext(ctx, `DELETE FROM tenant_extract_configs WHERE tenant_id = ?`, tenantID)
	return err
}
