package sqlite

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"math"
	"sort"
	"strings"
	"time"

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

type WorkspaceStore struct{}

func NewWorkspaceStore() *WorkspaceStore { return &WorkspaceStore{} }

// ── columns ────────────────────────────────

const workspaceCols = `id, owner_type, owner_id, name, root_path, max_bytes, status, indexing_enabled, git_remote_url, git_branch, git_credential_id, git_last_sync, created_at, updated_at`
const workspaceFileCols = `id, workspace_id, path, is_directory, content_type, size_bytes, sha256, index_status, chunk_count, created_at, updated_at`

// ── scanners ───────────────────────────────

func scanWorkspace(row interface{ Scan(dest ...interface{}) error }) (*models.Workspace, error) {
	var w models.Workspace
	var maxBytes sql.NullInt64
	var gitRemote, gitBranch, gitCredID sql.NullString
	err := row.Scan(
		&w.ID, &w.OwnerType, &w.OwnerID, &w.Name,
		&w.RootPath, &maxBytes, &w.Status, &w.IndexingEnabled,
		&gitRemote, &gitBranch, &gitCredID, stN(&w.GitLastSync),
		st(&w.CreatedAt), st(&w.UpdatedAt),
	)
	if err != nil {
		return nil, err
	}
	if maxBytes.Valid {
		w.MaxBytes = &maxBytes.Int64
	}
	if gitRemote.Valid {
		w.GitRemoteURL = &gitRemote.String
	}
	if gitBranch.Valid {
		w.GitBranch = &gitBranch.String
	}
	if gitCredID.Valid {
		w.GitCredentialID = &gitCredID.String
	}
	return &w, nil
}

func scanWorkspaceFile(row interface{ Scan(dest ...interface{}) error }) (*models.WorkspaceFile, error) {
	var f models.WorkspaceFile
	var contentType sql.NullString
	var sha256 sql.NullString
	var indexStatus sql.NullString
	err := row.Scan(
		&f.ID, &f.WorkspaceID, &f.Path, &f.IsDirectory,
		&contentType, &f.SizeBytes, &sha256,
		&indexStatus, &f.ChunkCount,
		st(&f.CreatedAt), st(&f.UpdatedAt),
	)
	if err != nil {
		return nil, err
	}
	if contentType.Valid {
		f.ContentType = contentType.String
	}
	if sha256.Valid {
		f.SHA256 = sha256.String
	}
	if indexStatus.Valid {
		f.IndexStatus = indexStatus.String
	}
	return &f, nil
}

// ── Workspace CRUD ─────────────────────────

func (s *WorkspaceStore) Create(ctx context.Context, w *models.Workspace) error {
	if w.ID == "" {
		w.ID = store.NewID()
	}
	now := time.Now().UTC()
	w.CreatedAt = now
	w.UpdatedAt = now
	_, err := DB.ExecContext(ctx, `
		INSERT INTO workspaces (id, owner_type, owner_id, name, root_path, max_bytes, status, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		w.ID, w.OwnerType, w.OwnerID, w.Name, w.RootPath,
		w.MaxBytes, w.Status,
		now.Format(timeFmt), now.Format(timeFmt),
	)
	return err
}

func (s *WorkspaceStore) GetByID(ctx context.Context, id string) (*models.Workspace, error) {
	row := DB.QueryRowContext(ctx,
		`SELECT `+workspaceCols+` FROM workspaces WHERE id = ?`, id)
	return scanWorkspace(row)
}

func (s *WorkspaceStore) Update(ctx context.Context, id string, patch models.WorkspacePatch) error {
	b := NewUpdate("workspaces")
	if patch.Name != nil {
		b.Set("name", *patch.Name)
	}
	if patch.MaxBytes != nil {
		b.Set("max_bytes", *patch.MaxBytes)
	}
	if patch.Status != nil {
		b.Set("status", *patch.Status)
	}
	if patch.IndexingEnabled != nil {
		b.Set("indexing_enabled", *patch.IndexingEnabled)
	}
	if patch.GitRemoteURL != nil {
		b.Set("git_remote_url", *patch.GitRemoteURL)
	}
	if patch.GitBranch != nil {
		b.Set("git_branch", *patch.GitBranch)
	}
	if patch.GitCredentialID != nil {
		b.Set("git_credential_id", *patch.GitCredentialID)
	}
	if !b.HasSets() {
		return nil
	}
	b.SetExpr("updated_at", nowExpr)
	b.Where("id", id)
	_, err := b.Exec(DB)
	return err
}

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

// ── Ownership lookup ───────────────────────

func (s *WorkspaceStore) GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error) {
	row := DB.QueryRowContext(ctx,
		`SELECT `+workspaceCols+` FROM workspaces
		 WHERE owner_type = ? AND owner_id = ? AND status != 'deleting'
		 ORDER BY created_at DESC LIMIT 1`,
		ownerType, ownerID)
	return scanWorkspace(row)
}

func (s *WorkspaceStore) ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error) {
	rows, err := DB.QueryContext(ctx,
		`SELECT `+workspaceCols+` FROM workspaces
		 WHERE owner_type = ? AND owner_id = ? AND status != 'deleting'
		 ORDER BY created_at DESC`,
		ownerType, ownerID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []models.Workspace
	for rows.Next() {
		w, err := scanWorkspace(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, *w)
	}
	return out, rows.Err()
}

// ── File index ─────────────────────────────

func (s *WorkspaceStore) UpsertFile(ctx context.Context, f *models.WorkspaceFile) error {
	if f.ID == "" {
		f.ID = store.NewID()
	}
	now := time.Now().UTC()
	f.CreatedAt = now
	f.UpdatedAt = now

	_, err := DB.ExecContext(ctx, `
		INSERT INTO workspace_files (id, workspace_id, path, is_directory, content_type, size_bytes, sha256, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT (workspace_id, path) DO UPDATE SET
			is_directory = excluded.is_directory,
			content_type = excluded.content_type,
			size_bytes   = excluded.size_bytes,
			sha256       = excluded.sha256,
			updated_at   = datetime('now')`,
		f.ID, f.WorkspaceID, f.Path, f.IsDirectory,
		models.NullString(&f.ContentType), f.SizeBytes,
		models.NullString(&f.SHA256),
		now.Format(timeFmt), now.Format(timeFmt),
	)
	return err
}

func (s *WorkspaceStore) DeleteFile(ctx context.Context, workspaceID, path string) error {
	_, err := DB.ExecContext(ctx,
		`DELETE FROM workspace_files WHERE workspace_id = ? AND path = ?`,
		workspaceID, path)
	return err
}

func (s *WorkspaceStore) DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error {
	_, err := DB.ExecContext(ctx,
		`DELETE FROM workspace_files WHERE workspace_id = ? AND path LIKE ?`,
		workspaceID, prefix+"%")
	return err
}

func (s *WorkspaceStore) GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error) {
	row := DB.QueryRowContext(ctx,
		`SELECT `+workspaceFileCols+` FROM workspace_files
		 WHERE workspace_id = ? AND path = ?`,
		workspaceID, path)
	return scanWorkspaceFile(row)
}

func (s *WorkspaceStore) ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error) {
	var query string
	var args []interface{}

	if recursive {
		if prefix == "" {
			query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
				WHERE workspace_id = ? ORDER BY path`
			args = []interface{}{workspaceID}
		} else {
			query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
				WHERE workspace_id = ? AND path LIKE ? ORDER BY path`
			args = []interface{}{workspaceID, prefix + "%"}
		}
	} else {
		if prefix == "" {
			query = `SELECT ` + workspaceFileCols + ` FROM workspace_files
				WHERE workspace_id = ? AND path NOT LIKE '%/%' ORDER BY path`
			args = []interface{}{workspaceID}
		} else {
			// Direct children: starts with prefix, no slash after prefix
			query = fmt.Sprintf(`SELECT `+workspaceFileCols+` FROM workspace_files
				WHERE workspace_id = ? AND path LIKE ?
				AND substr(path, %d) NOT LIKE '%%/%%'
				ORDER BY path`, len(prefix)+1)
			args = []interface{}{workspaceID, prefix + "%"}
		}
	}

	rows, err := DB.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []models.WorkspaceFile
	for rows.Next() {
		f, err := scanWorkspaceFile(rows)
		if err != nil {
			return nil, err
		}
		out = append(out, *f)
	}
	return out, rows.Err()
}

func (s *WorkspaceStore) DeleteAllFiles(ctx context.Context, workspaceID string) error {
	_, err := DB.ExecContext(ctx,
		`DELETE FROM workspace_files WHERE workspace_id = ?`, workspaceID)
	return err
}

// ── Stats ──────────────────────────────────

func (s *WorkspaceStore) GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error) {
	stats := &models.WorkspaceStats{WorkspaceID: workspaceID}
	err := DB.QueryRowContext(ctx, `
		SELECT
			COALESCE(SUM(CASE WHEN NOT is_directory THEN 1 ELSE 0 END), 0),
			COALESCE(SUM(CASE WHEN is_directory THEN 1 ELSE 0 END), 0),
			COALESCE(SUM(size_bytes), 0)
		FROM workspace_files WHERE workspace_id = ?`,
		workspaceID,
	).Scan(&stats.FileCount, &stats.DirCount, &stats.TotalBytes)
	if err != nil {
		return nil, err
	}

	var maxBytes sql.NullInt64
	err = DB.QueryRowContext(ctx,
		`SELECT max_bytes FROM workspaces WHERE id = ?`, workspaceID,
	).Scan(&maxBytes)
	if err != nil && err != sql.ErrNoRows {
		return nil, err
	}
	if maxBytes.Valid {
		stats.MaxBytes = &maxBytes.Int64
	}

	return stats, nil
}

// ── Chunks (v0.21.2) ─────────────────────────

func (s *WorkspaceStore) InsertChunks(ctx context.Context, chunks []models.WorkspaceChunk) error {
	if len(chunks) == 0 {
		return nil
	}

	valueParts := make([]string, 0, len(chunks))
	args := make([]interface{}, 0, len(chunks)*8)

	for _, c := range chunks {
		c.ID = store.NewID()
		valueParts = append(valueParts, "(?, ?, ?, ?, ?, ?, ?, ?)")
		var embJSON interface{}
		if len(c.Embedding) > 0 {
			embJSON = ToJSON(c.Embedding)
		}
		args = append(args, c.ID, c.WorkspaceID, c.FileID, c.ChunkIndex,
			c.Content, c.TokenCount, embJSON, ToJSON(c.Metadata))
	}

	q := fmt.Sprintf(`INSERT INTO workspace_chunks
		(id, workspace_id, file_id, chunk_index, content, token_count, embedding, metadata)
		VALUES %s`, strings.Join(valueParts, ", "))

	_, err := DB.ExecContext(ctx, q, args...)
	return err
}

func (s *WorkspaceStore) DeleteChunksByFile(ctx context.Context, fileID string) error {
	_, err := DB.ExecContext(ctx, `DELETE FROM workspace_chunks WHERE file_id = ?`, fileID)
	return err
}

// SimilaritySearch performs app-level cosine similarity for SQLite.
// Loads all chunks for the workspace, computes cosine distance in Go,
// and returns the top results above threshold.
func (s *WorkspaceStore) SimilaritySearch(ctx context.Context, workspaceID string, queryVec []float64, threshold float64, limit int) ([]models.WorkspaceChunkResult, error) {
	if len(queryVec) == 0 {
		return nil, nil
	}
	if limit <= 0 {
		limit = 10
	}

	rows, err := DB.QueryContext(ctx, `
		SELECT f.path, c.content, c.embedding, c.metadata
		FROM workspace_chunks c
		JOIN workspace_files f ON c.file_id = f.id
		WHERE c.workspace_id = ?
		  AND c.embedding IS NOT NULL`,
		workspaceID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	type candidate struct {
		result     models.WorkspaceChunkResult
		similarity float64
	}
	var candidates []candidate

	for rows.Next() {
		var filePath, content, embJSON string
		var metaJSON sql.NullString
		if err := rows.Scan(&filePath, &content, &embJSON, &metaJSON); err != nil {
			return nil, err
		}

		var embedding []float64
		if err := json.Unmarshal([]byte(embJSON), &embedding); err != nil {
			continue // skip malformed
		}

		sim := wsCosineSimilarity(queryVec, embedding)
		if sim > threshold {
			r := models.WorkspaceChunkResult{
				FilePath: filePath,
				Content:  content,
				Score:    sim,
			}
			if metaJSON.Valid {
				ScanJSON(metaJSON.String, &r.Metadata)
			}
			if r.Metadata != nil {
				if lh, ok := r.Metadata["line_start"]; ok {
					if n, ok := lh.(float64); ok {
						r.LineHint = int(n)
					}
				}
			}
			candidates = append(candidates, candidate{result: r, similarity: sim})
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	sort.Slice(candidates, func(i, j int) bool {
		return candidates[i].similarity > candidates[j].similarity
	})

	if len(candidates) > limit {
		candidates = candidates[:limit]
	}

	results := make([]models.WorkspaceChunkResult, len(candidates))
	for i, c := range candidates {
		results[i] = c.result
	}
	return results, nil
}

func (s *WorkspaceStore) UpdateFileIndexStatus(ctx context.Context, fileID, status string, chunkCount int) error {
	_, err := DB.ExecContext(ctx,
		`UPDATE workspace_files SET index_status = ?, chunk_count = ?, updated_at = datetime('now') WHERE id = ?`,
		status, chunkCount, fileID)
	return err
}

func (s *WorkspaceStore) SetGitLastSync(ctx context.Context, workspaceID string, t time.Time) error {
	_, err := DB.ExecContext(ctx,
		`UPDATE workspaces SET git_last_sync = ?, updated_at = datetime('now') WHERE id = ?`,
		t.Format(timeFmt), workspaceID)
	return err
}

// wsCosineSimilarity computes cosine similarity between two vectors.
// Named to avoid collision with the KB package-level function.
func wsCosineSimilarity(a, b []float64) float64 {
	if len(a) != len(b) || len(a) == 0 {
		return 0
	}
	var dot, normA, normB float64
	for i := range a {
		dot += a[i] * b[i]
		normA += a[i] * a[i]
		normB += b[i] * b[i]
	}
	if normA == 0 || normB == 0 {
		return 0
	}
	return dot / (math.Sqrt(normA) * math.Sqrt(normB))
}