package postgres

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

	"chat-switchboard/models"
)

type PersonaStore struct{}

func NewPersonaStore() *PersonaStore { return &PersonaStore{} }

const personaCols = `id, name, handle, description, icon, avatar, base_model_id, provider_config_id,
	system_prompt, temperature, max_tokens, thinking_budget, top_p,
	scope, owner_id, created_by, is_active, is_shared, memory_enabled, memory_extraction_prompt,
	created_at, updated_at`

func (s *PersonaStore) Create(ctx context.Context, p *models.Persona) error {
	// Auto-generate handle from name if not set
	if p.Handle == "" {
		p.Handle = models.HandleFromName(p.Name)
	}
	return DB.QueryRowContext(ctx, `
		INSERT INTO personas (name, handle, description, icon, avatar, base_model_id, provider_config_id,
			system_prompt, temperature, max_tokens, thinking_budget, top_p,
			scope, owner_id, created_by, is_active, is_shared,
			memory_enabled, memory_extraction_prompt)
		VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19)
		RETURNING id, created_at, updated_at`,
		p.Name, p.Handle, p.Description, p.Icon, p.Avatar, p.BaseModelID,
		models.NullString(p.ProviderConfigID),
		p.SystemPrompt, models.NullFloat(p.Temperature), models.NullInt(p.MaxTokens),
		models.NullInt(p.ThinkingBudget), models.NullFloat(p.TopP),
		p.Scope, models.NullString(p.OwnerID), p.CreatedBy, p.IsActive, p.IsShared,
		p.MemoryEnabled, p.MemoryExtractionPrompt,
	).Scan(&p.ID, &p.CreatedAt, &p.UpdatedAt)
}

func (s *PersonaStore) GetByID(ctx context.Context, id string) (*models.Persona, error) {
	row := DB.QueryRowContext(ctx,
		fmt.Sprintf("SELECT %s FROM personas WHERE id = $1", personaCols), id)
	p, err := scanPersona(row)
	if err != nil {
		return nil, err
	}
	// Load grants
	grants, _ := s.GetGrants(ctx, id)
	p.Grants = grants
	return p, nil
}

func (s *PersonaStore) Update(ctx context.Context, id string, patch models.PersonaPatch) error {
	b := NewUpdate("personas")
	if patch.Name != nil {
		b.Set("name", *patch.Name)
	}
	if patch.Handle != nil {
		b.Set("handle", *patch.Handle)
	}
	if patch.Description != nil {
		b.Set("description", *patch.Description)
	}
	if patch.Icon != nil {
		b.Set("icon", *patch.Icon)
	}
	if patch.Avatar != nil {
		b.Set("avatar", *patch.Avatar)
	}
	if patch.BaseModelID != nil {
		b.Set("base_model_id", *patch.BaseModelID)
	}
	if patch.ProviderConfigID != nil {
		b.Set("provider_config_id", models.NullString(patch.ProviderConfigID))
	}
	if patch.SystemPrompt != nil {
		b.Set("system_prompt", *patch.SystemPrompt)
	}
	if patch.Temperature != nil {
		b.Set("temperature", models.NullFloat(patch.Temperature))
	}
	if patch.MaxTokens != nil {
		b.Set("max_tokens", models.NullInt(patch.MaxTokens))
	}
	if patch.ThinkingBudget != nil {
		b.Set("thinking_budget", models.NullInt(patch.ThinkingBudget))
	}
	if patch.TopP != nil {
		b.Set("top_p", models.NullFloat(patch.TopP))
	}
	if patch.IsActive != nil {
		b.Set("is_active", *patch.IsActive)
	}
	if patch.IsShared != nil {
		b.Set("is_shared", *patch.IsShared)
	}
	if patch.MemoryEnabled != nil {
		b.Set("memory_enabled", *patch.MemoryEnabled)
	}
	if patch.MemoryExtractionPrompt != nil {
		b.Set("memory_extraction_prompt", *patch.MemoryExtractionPrompt)
	}
	if !b.HasSets() {
		return nil
	}
	b.Where("id", id)
	_, err := b.Exec(DB)
	return err
}

func (s *PersonaStore) Delete(ctx context.Context, id string) error {
	_, err := DB.ExecContext(ctx, "DELETE FROM personas WHERE id = $1", id)
	return err
}

// ListForUser returns all Personas visible to a user:
// global active + team-scoped (for user's teams) + personal + shared + group-granted.
func (s *PersonaStore) ListForUser(ctx context.Context, userID string) ([]models.Persona, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf(`SELECT %s FROM personas WHERE is_active = true AND (
			scope = 'global'
			OR (scope = 'personal' AND created_by = $1)
			OR (scope = 'team' AND owner_id IN (
				SELECT team_id FROM team_members WHERE user_id = $1
			))
			OR (scope = 'personal' AND is_shared = true)
			OR id IN (
				SELECT rg.resource_id FROM resource_grants rg
				WHERE rg.resource_type = 'persona'
				  AND (
					rg.grant_scope = 'global'
					OR (rg.grant_scope = 'groups'
					    AND EXISTS (
					        SELECT 1 FROM group_members gm
					        WHERE gm.user_id = $1
					          AND gm.group_id = ANY(rg.granted_groups)
					    ))
				  )
			)
		) ORDER BY scope, name`, personaCols), userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanPersonas(rows)
}

func (s *PersonaStore) ListForTeam(ctx context.Context, teamID string) ([]models.Persona, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf("SELECT %s FROM personas WHERE scope = 'team' AND owner_id = $1 AND is_active = true ORDER BY name", personaCols),
		teamID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanPersonas(rows)
}

func (s *PersonaStore) ListGlobal(ctx context.Context) ([]models.Persona, error) {
	rows, err := DB.QueryContext(ctx,
		fmt.Sprintf("SELECT %s FROM personas WHERE scope = 'global' ORDER BY name", personaCols))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanPersonas(rows)
}

// ── Grants ──────────────────────────────────

// SetGrants replaces all grants for a Persona (delete + re-insert).
func (s *PersonaStore) SetGrants(ctx context.Context, personaID string, grants []models.Grant) error {
	tx, err := DB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	_, err = tx.ExecContext(ctx, "DELETE FROM persona_grants WHERE persona_id = $1", personaID)
	if err != nil {
		return err
	}

	for _, g := range grants {
		configJSON := ToJSON(g.Config)
		_, err = tx.ExecContext(ctx, `
			INSERT INTO persona_grants (persona_id, grant_type, grant_ref, config)
			VALUES ($1, $2, $3, $4)`,
			personaID, g.GrantType, g.GrantRef, configJSON)
		if err != nil {
			return fmt.Errorf("grant %s/%s: %w", g.GrantType, g.GrantRef, err)
		}
	}

	return tx.Commit()
}

func (s *PersonaStore) GetGrants(ctx context.Context, personaID string) ([]models.Grant, error) {
	rows, err := DB.QueryContext(ctx,
		"SELECT id, persona_id, grant_type, grant_ref, config, created_at FROM persona_grants WHERE persona_id = $1 ORDER BY grant_type, grant_ref",
		personaID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []models.Grant
	for rows.Next() {
		var g models.Grant
		var configJSON []byte
		err := rows.Scan(&g.ID, &g.PersonaID, &g.GrantType, &g.GrantRef, &configJSON, &g.CreatedAt)
		if err != nil {
			return nil, err
		}
		json.Unmarshal(configJSON, &g.Config)
		result = append(result, g)
	}
	return result, rows.Err()
}

// GetToolGrants returns just the tool names for a Persona.
func (s *PersonaStore) GetToolGrants(ctx context.Context, personaID string) ([]string, error) {
	rows, err := DB.QueryContext(ctx,
		"SELECT grant_ref FROM persona_grants WHERE persona_id = $1 AND grant_type = 'tool' ORDER BY grant_ref",
		personaID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []string
	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			return nil, err
		}
		result = append(result, name)
	}
	return result, rows.Err()
}

// UserCanAccess checks if a user can see/use a specific Persona.
func (s *PersonaStore) UserCanAccess(ctx context.Context, userID, personaID string) (bool, error) {
	var exists bool
	err := DB.QueryRowContext(ctx, `
		SELECT EXISTS(
			SELECT 1 FROM personas WHERE id = $2 AND is_active = true AND (
				scope = 'global'
				OR (scope = 'personal' AND created_by = $1)
				OR (scope = 'team' AND owner_id IN (
					SELECT team_id FROM team_members WHERE user_id = $1
				))
				OR (scope = 'personal' AND is_shared = true)
				OR id IN (
					SELECT rg.resource_id FROM resource_grants rg
					WHERE rg.resource_type = 'persona'
					  AND rg.resource_id = $2
					  AND (
						rg.grant_scope = 'global'
						OR (rg.grant_scope = 'groups'
						    AND EXISTS (
						        SELECT 1 FROM group_members gm
						        WHERE gm.user_id = $1
						          AND gm.group_id = ANY(rg.granted_groups)
						    ))
					  )
				)
			)
		)`, userID, personaID).Scan(&exists)
	return exists, err
}

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

func scanPersona(row *sql.Row) (*models.Persona, error) {
	var p models.Persona
	var providerConfigID, ownerID sql.NullString
	var handle sql.NullString
	var temp, topP sql.NullFloat64
	var maxTokens, thinkingBudget sql.NullInt64
	err := row.Scan(
		&p.ID, &p.Name, &handle, &p.Description, &p.Icon, &p.Avatar,
		&p.BaseModelID, &providerConfigID,
		&p.SystemPrompt, &temp, &maxTokens, &thinkingBudget, &topP,
		&p.Scope, &ownerID, &p.CreatedBy, &p.IsActive, &p.IsShared,
		&p.MemoryEnabled, &p.MemoryExtractionPrompt,
		&p.CreatedAt, &p.UpdatedAt,
	)
	if err != nil {
		return nil, err
	}
	if handle.Valid {
		p.Handle = handle.String
	}
	p.ProviderConfigID = NullableStringPtr(providerConfigID)
	p.OwnerID = NullableStringPtr(ownerID)
	p.Temperature = NullableFloat64Ptr(temp)
	p.MaxTokens = NullableIntPtr(maxTokens)
	p.ThinkingBudget = NullableIntPtr(thinkingBudget)
	p.TopP = NullableFloat64Ptr(topP)
	return &p, nil
}

func scanPersonas(rows *sql.Rows) ([]models.Persona, error) {
	var result []models.Persona
	for rows.Next() {
		var p models.Persona
		var providerConfigID, ownerID sql.NullString
		var handle sql.NullString
		var temp, topP sql.NullFloat64
		var maxTokens, thinkingBudget sql.NullInt64
		err := rows.Scan(
			&p.ID, &p.Name, &handle, &p.Description, &p.Icon, &p.Avatar,
			&p.BaseModelID, &providerConfigID,
			&p.SystemPrompt, &temp, &maxTokens, &thinkingBudget, &topP,
			&p.Scope, &ownerID, &p.CreatedBy, &p.IsActive, &p.IsShared,
			&p.MemoryEnabled, &p.MemoryExtractionPrompt,
			&p.CreatedAt, &p.UpdatedAt,
		)
		if err != nil {
			return nil, err
		}
		if handle.Valid {
			p.Handle = handle.String
		}
		p.ProviderConfigID = NullableStringPtr(providerConfigID)
		p.OwnerID = NullableStringPtr(ownerID)
		p.Temperature = NullableFloat64Ptr(temp)
		p.MaxTokens = NullableIntPtr(maxTokens)
		p.ThinkingBudget = NullableIntPtr(thinkingBudget)
		p.TopP = NullableFloat64Ptr(topP)
		result = append(result, p)
	}
	return result, rows.Err()
}

// ── Persona-KB Bindings (v0.17.0) ───────────

func (s *PersonaStore) SetKBs(ctx context.Context, personaID string, kbIDs []string, autoSearch map[string]bool) error {
	tx, err := DB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	_, err = tx.ExecContext(ctx, "DELETE FROM persona_knowledge_bases WHERE persona_id = $1", personaID)
	if err != nil {
		return err
	}

	for _, kbID := range kbIDs {
		auto := false
		if autoSearch != nil {
			auto = autoSearch[kbID]
		}
		_, err = tx.ExecContext(ctx, `
			INSERT INTO persona_knowledge_bases (persona_id, kb_id, auto_search)
			VALUES ($1, $2, $3)`,
			personaID, kbID, auto)
		if err != nil {
			return fmt.Errorf("persona KB %s: %w", kbID, err)
		}
	}

	return tx.Commit()
}

func (s *PersonaStore) GetKBs(ctx context.Context, personaID string) ([]models.PersonaKB, error) {
	rows, err := DB.QueryContext(ctx, `
		SELECT pkb.persona_id, pkb.kb_id, pkb.auto_search, pkb.added_at,
		       kb.name AS kb_name, kb.document_count, kb.chunk_count
		FROM persona_knowledge_bases pkb
		JOIN knowledge_bases kb ON kb.id = pkb.kb_id
		WHERE pkb.persona_id = $1
		ORDER BY kb.name`, personaID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []models.PersonaKB
	for rows.Next() {
		var pkb models.PersonaKB
		if err := rows.Scan(&pkb.PersonaID, &pkb.KBID, &pkb.AutoSearch, &pkb.AddedAt,
			&pkb.KBName, &pkb.DocumentCount, &pkb.ChunkCount); err != nil {
			return nil, err
		}
		result = append(result, pkb)
	}
	if result == nil {
		result = make([]models.PersonaKB, 0)
	}
	return result, rows.Err()
}

func (s *PersonaStore) GetKBIDs(ctx context.Context, personaID string) ([]string, error) {
	rows, err := DB.QueryContext(ctx,
		"SELECT kb_id FROM persona_knowledge_bases WHERE persona_id = $1", personaID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var ids []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	if ids == nil {
		ids = make([]string, 0)
	}
	return ids, rows.Err()
}
