package sqlite

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"time"

	"chat-switchboard/models"
	"chat-switchboard/store"
)

type CatalogStore struct{}

func NewCatalogStore() *CatalogStore { return &CatalogStore{} }

const catalogCols = `id, provider_config_id, model_id, display_name, model_type,
	capabilities, pricing, visibility, last_synced_at, created_at, updated_at`

// catalogColsMC is catalogCols with mc. prefix for use in JOINs
// where id/created_at/updated_at are ambiguous.
const catalogColsMC = `mc.id, mc.provider_config_id, mc.model_id, mc.display_name, mc.model_type,
	mc.capabilities, mc.pricing, mc.visibility, mc.last_synced_at, mc.created_at, mc.updated_at`

// UpsertFromSync bulk-inserts or updates catalog entries from a provider API fetch.
// New models default to 'disabled' visibility (secure by default).
//
// Runs in a single transaction: one SELECT to identify existing models,
// then N upserts via a prepared INSERT ON CONFLICT statement, one COMMIT.
// SQLite with WAL benefits enormously — one journal cycle instead of N.
func (s *CatalogStore) UpsertFromSync(ctx context.Context, providerConfigID string, entries []store.CatalogSyncEntry) (added, updated int, err error) {
	if len(entries) == 0 {
		return 0, 0, nil
	}

	tx, err := DB.BeginTx(ctx, nil)
	if err != nil {
		return 0, 0, fmt.Errorf("begin tx: %w", err)
	}
	defer tx.Rollback()

	// Pre-fetch existing model_ids for this provider to track added vs updated.
	existing := make(map[string]bool)
	rows, err := tx.QueryContext(ctx,
		"SELECT model_id FROM model_catalog WHERE provider_config_id = ?",
		providerConfigID)
	if err != nil {
		return 0, 0, fmt.Errorf("list existing: %w", err)
	}
	for rows.Next() {
		var mid string
		if err := rows.Scan(&mid); err != nil {
			rows.Close()
			return 0, 0, fmt.Errorf("scan existing: %w", err)
		}
		existing[mid] = true
	}
	rows.Close()
	if err := rows.Err(); err != nil {
		return 0, 0, fmt.Errorf("list existing: %w", err)
	}

	// Prepare the upsert statement once, execute N times.
	// SQLite needs an explicit id value (TEXT PK, no auto-generation).
	// On conflict the id is preserved — only metadata columns update.
	stmt, err := tx.PrepareContext(ctx, `
		INSERT INTO model_catalog (id, provider_config_id, model_id, display_name,
			model_type, capabilities, pricing, visibility, last_synced_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, 'disabled', ?)
		ON CONFLICT (provider_config_id, model_id) DO UPDATE SET
			display_name   = excluded.display_name,
			model_type     = excluded.model_type,
			capabilities   = excluded.capabilities,
			pricing        = excluded.pricing,
			last_synced_at = excluded.last_synced_at`)
	if err != nil {
		return 0, 0, fmt.Errorf("prepare upsert: %w", err)
	}
	defer stmt.Close()

	now := time.Now().UTC().Format("2006-01-02 15:04:05")
	for _, e := range entries {
		capsJSON := ToJSON(e.Capabilities)
		pricingJSON := ToJSON(e.Pricing)

		modelType := e.ModelType
		if modelType == "" {
			modelType = "chat"
		}

		if _, err := stmt.ExecContext(ctx,
			store.NewID(), providerConfigID, e.ModelID, e.DisplayName,
			modelType, capsJSON, pricingJSON, now,
		); err != nil {
			return added, updated, fmt.Errorf("upsert %s: %w", e.ModelID, err)
		}

		if existing[e.ModelID] {
			updated++
		} else {
			added++
		}
	}

	if err := tx.Commit(); err != nil {
		return 0, 0, fmt.Errorf("commit: %w", err)
	}
	return added, updated, nil
}

func (s *CatalogStore) GetByID(ctx context.Context, id string) (*models.CatalogEntry, error) {
	row := DB.QueryRowContext(ctx,
		fmt.Sprintf("SELECT %s FROM model_catalog WHERE id = ?", catalogCols), id)
	return scanCatalogEntry(row)
}

func (s *CatalogStore) GetByModelID(ctx context.Context, providerConfigID, modelID string) (*models.CatalogEntry, error) {
	row := DB.QueryRowContext(ctx,
		fmt.Sprintf("SELECT %s FROM model_catalog WHERE provider_config_id = ? AND model_id = ?", catalogCols),
		providerConfigID, modelID)
	return scanCatalogEntry(row)
}

// GetByModelIDAny returns the most recently synced catalog entry for a model_id
// across any provider. Used to resolve capabilities for personas with auto-resolve
// (no specific provider_config_id).
func (s *CatalogStore) GetByModelIDAny(ctx context.Context, modelID string) (*models.CatalogEntry, error) {
	row := DB.QueryRowContext(ctx,
		fmt.Sprintf("SELECT %s FROM model_catalog WHERE model_id = ? ORDER BY last_synced_at DESC NULLS LAST LIMIT 1", catalogCols),
		modelID)
	return scanCatalogEntry(row)
}

func (s *CatalogStore) ListVisible(ctx context.Context) ([]models.CatalogEntry, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf(`SELECT %s FROM model_catalog mc
			JOIN provider_configs pc ON pc.id = mc.provider_config_id
			WHERE mc.visibility = 'enabled' AND pc.scope = 'global' AND pc.is_active = 1
			ORDER BY mc.model_id`, catalogColsMC))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCatalogEntries(rows)
}

func (s *CatalogStore) ListForProvider(ctx context.Context, providerConfigID string) ([]models.CatalogEntry, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf("SELECT %s FROM model_catalog WHERE provider_config_id = ? ORDER BY model_id", catalogCols),
		providerConfigID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCatalogEntries(rows)
}

func (s *CatalogStore) ListEnabledForProvider(ctx context.Context, providerConfigID string) ([]models.CatalogEntry, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf("SELECT %s FROM model_catalog WHERE provider_config_id = ? AND visibility = 'enabled' ORDER BY model_id", catalogCols),
		providerConfigID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCatalogEntries(rows)
}

func (s *CatalogStore) ListAll(ctx context.Context) ([]models.CatalogEntry, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf("SELECT %s FROM model_catalog ORDER BY model_id", catalogCols))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCatalogEntries(rows)
}

// ListAllGlobal returns all catalog entries whose provider_config has scope='global'.
// Used by admin panel — admin should not see user BYOK or team-level models.
func (s *CatalogStore) ListAllGlobal(ctx context.Context) ([]models.CatalogEntry, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf(`SELECT %s FROM model_catalog mc
			JOIN provider_configs pc ON pc.id = mc.provider_config_id
			WHERE pc.scope = 'global'
			ORDER BY mc.model_id`, catalogColsMC))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanCatalogEntries(rows)
}

func (s *CatalogStore) SetVisibility(ctx context.Context, id string, visibility string) error {
	_, err := DB.ExecContext(ctx,
		"UPDATE model_catalog SET visibility = ? WHERE id = ?", visibility, id)
	return err
}

func (s *CatalogStore) BulkSetVisibility(ctx context.Context, providerConfigID string, visibility string) error {
	_, err := DB.ExecContext(ctx,
		"UPDATE model_catalog SET visibility = ? WHERE provider_config_id = ?",
		visibility, providerConfigID)
	return err
}

func (s *CatalogStore) BulkSetVisibilityAll(ctx context.Context, visibility string) error {
	_, err := DB.ExecContext(ctx,
		`UPDATE model_catalog SET visibility = ?
		 WHERE provider_config_id IN (
		     SELECT id FROM provider_configs WHERE scope = 'global'
		 )`, visibility)
	return err
}

func (s *CatalogStore) Delete(ctx context.Context, id string) error {
	_, err := DB.ExecContext(ctx, "DELETE FROM model_catalog WHERE id = ?", id)
	return err
}

func (s *CatalogStore) DeleteForProvider(ctx context.Context, providerConfigID string) error {
	_, err := DB.ExecContext(ctx,
		"DELETE FROM model_catalog WHERE provider_config_id = ?", providerConfigID)
	return err
}

// ── Scanners ────────────────────────────────

func scanCatalogEntry(row *sql.Row) (*models.CatalogEntry, error) {
	var e models.CatalogEntry
	var capsJSON, pricingJSON []byte
	var displayName sql.NullString
	var lastSynced *time.Time
	err := row.Scan(
		&e.ID, &e.ProviderConfigID, &e.ModelID, &displayName, &e.ModelType,
		&capsJSON, &pricingJSON, &e.Visibility, stN(&lastSynced),
		st(&e.CreatedAt), st(&e.UpdatedAt),
	)
	if err != nil {
		return nil, err
	}
	e.DisplayName = NullableString(displayName)
	json.Unmarshal(capsJSON, &e.Capabilities)
	if len(pricingJSON) > 0 {
		e.Pricing = &models.ModelPricing{}
		json.Unmarshal(pricingJSON, e.Pricing)
	}
	e.LastSyncedAt = lastSynced
	return &e, nil
}

func scanCatalogEntries(rows *sql.Rows) ([]models.CatalogEntry, error) {
	result := make([]models.CatalogEntry, 0) // never nil — serializes as [] not null
	for rows.Next() {
		var e models.CatalogEntry
		var capsJSON, pricingJSON []byte
		var displayName sql.NullString
		var lastSynced *time.Time
		err := rows.Scan(
			&e.ID, &e.ProviderConfigID, &e.ModelID, &displayName, &e.ModelType,
			&capsJSON, &pricingJSON, &e.Visibility, stN(&lastSynced),
			st(&e.CreatedAt), st(&e.UpdatedAt),
		)
		if err != nil {
			return nil, err
		}
		e.DisplayName = NullableString(displayName)
		json.Unmarshal(capsJSON, &e.Capabilities)
		if len(pricingJSON) > 0 {
			e.Pricing = &models.ModelPricing{}
			json.Unmarshal(pricingJSON, e.Pricing)
		}
		e.LastSyncedAt = lastSynced
		result = append(result, e)
	}
	return result, rows.Err()
}

// ── CS2 additions (v0.29.0) ─────────────────────────────────────────────

func (s *CatalogStore) GetCapabilities(ctx context.Context, modelID, configID string) ([]byte, error) {
	var capsJSON []byte
	err := DB.QueryRowContext(ctx, `
		SELECT capabilities FROM model_catalog
		WHERE model_id = ? AND provider_config_id = ?
	`, modelID, configID).Scan(&capsJSON)
	if err != nil {
		return nil, err
	}
	return capsJSON, nil
}

func (s *CatalogStore) GetCapabilitiesAny(ctx context.Context, modelID string) ([]byte, error) {
	var capsJSON []byte
	err := DB.QueryRowContext(ctx, `
		SELECT capabilities FROM model_catalog
		WHERE model_id = ? ORDER BY last_synced_at DESC LIMIT 1
	`, modelID).Scan(&capsJSON)
	if err != nil {
		return nil, err
	}
	return capsJSON, nil
}

// ── CS6 additions (v0.29.0) ─────────────────────────────────────────────

func (s *CatalogStore) ListTeamAvailable(ctx context.Context) ([]store.TeamAvailableModel, error) {
	rows, err := DB.QueryContext(ctx, `
		SELECT mc.id, mc.model_id, mc.display_name, mc.visibility,
		       ac.provider, ac.name AS provider_name
		FROM model_catalog mc
		JOIN provider_configs ac ON mc.provider_config_id = ac.id
		WHERE mc.visibility IN ('enabled', 'team')
		  AND ac.is_active = 1 AND ac.scope = 'global'
		ORDER BY ac.name, mc.model_id
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	results := make([]store.TeamAvailableModel, 0)
	for rows.Next() {
		var m store.TeamAvailableModel
		if err := rows.Scan(&m.ID, &m.ModelID, &m.DisplayName, &m.Visibility,
			&m.Provider, &m.ProviderName); err != nil {
			continue
		}
		results = append(results, m)
	}
	return results, rows.Err()
}
