// Package gorm provides GORM-based database operations for claude-mnemonic.
package gorm

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

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/lukaszraczylo/claude-mnemonic/pkg/models"
)

// SessionStore provides session-related database operations using GORM.
type SessionStore struct {
	db *gorm.DB
}

// NewSessionStore creates a new session store.
func NewSessionStore(store *Store) *SessionStore {
	return &SessionStore{db: store.DB}
}

// CreateSDKSession creates a new SDK session (idempotent - returns existing ID if exists).
// This is the KEY to how claude-mnemonic stays unified across hooks.
func (s *SessionStore) CreateSDKSession(ctx context.Context, claudeSessionID, project, userPrompt string) (int64, error) {
	now := time.Now()

	session := &SDKSession{
		ClaudeSessionID: claudeSessionID,
		SDKSessionID: func() sql.NullString {
			return sql.NullString{String: claudeSessionID, Valid: true}
		}(),
		Project: project,
		UserPrompt: func() sql.NullString {
			if userPrompt != "" {
				return sql.NullString{String: userPrompt, Valid: true}
			}
			return sql.NullString{Valid: false}
		}(),
		Status:         "active",
		StartedAt:      now.Format(time.RFC3339),
		StartedAtEpoch: now.UnixMilli(),
	}

	// CRITICAL: INSERT OR IGNORE makes this idempotent
	// Use OnConflict with DoNothing to achieve INSERT OR IGNORE behavior
	result := s.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns:   []clause.Column{{Name: "claude_session_id"}},
			DoNothing: true,
		}).
		Create(session)

	if result.Error != nil {
		return 0, result.Error
	}

	// Check if insert happened
	if result.RowsAffected == 0 {
		// Session exists - UPDATE project and user_prompt if we have non-empty values
		if project != "" {
			updates := map[string]interface{}{
				"project": project,
			}
			if userPrompt != "" {
				updates["user_prompt"] = userPrompt
			}
			if err := s.db.WithContext(ctx).
				Model(&SDKSession{}).
				Where("claude_session_id = ?", claudeSessionID).
				Updates(updates).Error; err != nil {
				return 0, fmt.Errorf("failed to update session: %w", err)
			}
		}

		// Fetch existing session
		var existing SDKSession
		err := s.db.WithContext(ctx).
			Where("claude_session_id = ?", claudeSessionID).
			First(&existing).Error
		if err != nil {
			return 0, err
		}
		return existing.ID, nil
	}

	return session.ID, nil
}

// GetSessionByID retrieves a session by its database ID.
func (s *SessionStore) GetSessionByID(ctx context.Context, id int64) (*models.SDKSession, error) {
	var sess SDKSession
	err := s.db.WithContext(ctx).First(&sess, id).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return toModelSDKSession(&sess), nil
}

// FindAnySDKSession finds any session by Claude session ID (any status).
func (s *SessionStore) FindAnySDKSession(ctx context.Context, claudeSessionID string) (*models.SDKSession, error) {
	var sess SDKSession
	err := s.db.WithContext(ctx).
		Where("claude_session_id = ?", claudeSessionID).
		First(&sess).Error
	if err == gorm.ErrRecordNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return toModelSDKSession(&sess), nil
}

// IncrementPromptCounter increments the prompt counter and returns the new value.
// Uses a single SQL query with RETURNING clause for optimal performance.
func (s *SessionStore) IncrementPromptCounter(ctx context.Context, id int64) (int, error) {
	// Use raw SQL with RETURNING to get updated value in single query
	// SQLite supports RETURNING since version 3.35.0 (2021-03-12)
	var newCounter int
	err := s.db.WithContext(ctx).Raw(`
		UPDATE sdk_sessions
		SET prompt_counter = COALESCE(prompt_counter, 0) + 1
		WHERE id = ?
		RETURNING prompt_counter
	`, id).Scan(&newCounter).Error

	if err != nil {
		// Fallback for older SQLite versions without RETURNING support
		if err.Error() == "near \"RETURNING\": syntax error" || newCounter == 0 {
			// Atomic increment
			updateErr := s.db.WithContext(ctx).
				Model(&SDKSession{}).
				Where("id = ?", id).
				Update("prompt_counter", gorm.Expr("COALESCE(prompt_counter, 0) + 1")).Error
			if updateErr != nil {
				return 0, updateErr
			}

			// Fetch updated value
			var sess SDKSession
			fetchErr := s.db.WithContext(ctx).
				Select("prompt_counter").
				First(&sess, id).Error
			if fetchErr != nil {
				return 0, fetchErr
			}
			return sess.PromptCounter, nil
		}
		return 0, err
	}

	return newCounter, nil
}

// GetPromptCounter returns the current prompt counter for a session.
func (s *SessionStore) GetPromptCounter(ctx context.Context, id int64) (int, error) {
	var sess SDKSession
	err := s.db.WithContext(ctx).
		Select("prompt_counter").
		First(&sess, id).Error
	if err != nil {
		return 0, err
	}
	return sess.PromptCounter, nil
}

// GetSessionsToday returns the count of sessions started today.
func (s *SessionStore) GetSessionsToday(ctx context.Context) (int, error) {
	// Get start of today in milliseconds
	now := time.Now()
	startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	startEpoch := startOfDay.UnixMilli()

	var count int64
	err := s.db.WithContext(ctx).
		Model(&SDKSession{}).
		Where("started_at_epoch >= ?", startEpoch).
		Count(&count).Error

	return int(count), err
}

// GetAllProjects returns all unique project names.
func (s *SessionStore) GetAllProjects(ctx context.Context) ([]string, error) {
	var projects []string
	err := s.db.WithContext(ctx).
		Model(&SDKSession{}).
		Distinct("project").
		Where("project IS NOT NULL AND project != ''").
		Order("project ASC").
		Pluck("project", &projects).Error

	return projects, err
}

// toModelSDKSession converts a GORM SDKSession to pkg/models.SDKSession.
func toModelSDKSession(sess *SDKSession) *models.SDKSession {
	return &models.SDKSession{
		ID:               sess.ID,
		ClaudeSessionID:  sess.ClaudeSessionID,
		SDKSessionID:     sess.SDKSessionID,
		Project:          sess.Project,
		UserPrompt:       sess.UserPrompt,
		WorkerPort:       sess.WorkerPort,
		PromptCounter:    int64(sess.PromptCounter),
		Status:           models.SessionStatus(sess.Status),
		StartedAt:        sess.StartedAt,
		StartedAtEpoch:   sess.StartedAtEpoch,
		CompletedAt:      sess.CompletedAt,
		CompletedAtEpoch: sess.CompletedAtEpoch,
	}
}
