package sqlite

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

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

// ── ProjectStore ───────────────────────────

type ProjectStore struct{}

func NewProjectStore() *ProjectStore { return &ProjectStore{} }

// ── CRUD ────────────────────────────────────

func (s *ProjectStore) Create(ctx context.Context, p *models.Project) error {
	p.ID = store.NewID()
	now := time.Now().UTC()
	p.CreatedAt = now
	p.UpdatedAt = now
	_, err := DB.ExecContext(ctx, `
		INSERT INTO projects (id, name, description, color, icon, scope, owner_id, team_id,
		                      is_archived, settings, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
		p.ID, p.Name, p.Description, p.Color, p.Icon, p.Scope,
		p.OwnerID, models.NullString(p.TeamID),
		boolToInt(p.IsArchived), ToJSON(p.Settings),
		now.Format(timeFmt), now.Format(timeFmt),
	)
	return err
}

func (s *ProjectStore) GetByID(ctx context.Context, id string) (*models.Project, error) {
	var p models.Project
	var teamID, workspaceID sql.NullString
	var archived int
	err := DB.QueryRowContext(ctx, `
		SELECT p.id, p.name, p.description, p.color, p.icon, p.scope,
		       p.owner_id, p.team_id, p.is_archived, p.workspace_id, p.settings,
		       p.created_at, p.updated_at,
		       (SELECT COUNT(*) FROM project_channels WHERE project_id = p.id),
		       (SELECT COUNT(*) FROM project_knowledge_bases WHERE project_id = p.id),
		       (SELECT COUNT(*) FROM project_notes WHERE project_id = p.id)
		FROM projects p
		WHERE p.id = ?`, id).Scan(
		&p.ID, &p.Name, &p.Description, &p.Color, &p.Icon, &p.Scope,
		&p.OwnerID, &teamID, &archived, &workspaceID, &p.Settings,
		st(&p.CreatedAt), st(&p.UpdatedAt),
		&p.ChannelCount, &p.KBCount, &p.NoteCount,
	)
	if err != nil {
		return nil, err
	}
	p.IsArchived = archived != 0
	p.TeamID = NullableStringPtr(teamID)
	p.WorkspaceID = NullableStringPtr(workspaceID)
	return &p, nil
}

func (s *ProjectStore) Update(ctx context.Context, id string, patch models.ProjectPatch) error {
	sets := []string{}
	args := []interface{}{}

	add := func(col string, val interface{}) {
		sets = append(sets, col+" = ?")
		args = append(args, val)
	}

	if patch.Name != nil {
		add("name", *patch.Name)
	}
	if patch.Description != nil {
		add("description", *patch.Description)
	}
	if patch.Color != nil {
		add("color", *patch.Color)
	}
	if patch.Icon != nil {
		add("icon", *patch.Icon)
	}
	if patch.IsArchived != nil {
		add("is_archived", boolToInt(*patch.IsArchived))
	}
	if patch.WorkspaceID != nil {
		add("workspace_id", models.NullString(patch.WorkspaceID))
	}
	if len(patch.Settings) > 0 {
		// Merge: read existing settings, overlay with patch values, write back.
		var existing models.JSONMap
		_ = DB.QueryRowContext(ctx, "SELECT settings FROM projects WHERE id = ?", id).Scan(&existing)
		if existing == nil {
			existing = make(models.JSONMap)
		}
		for k, v := range patch.Settings {
			existing[k] = v
		}
		add("settings", ToJSON(existing))
	}
	if len(sets) == 0 {
		return nil
	}

	query := "UPDATE projects SET "
	for i, s := range sets {
		if i > 0 {
			query += ", "
		}
		query += s
	}
	query += " WHERE id = ?"
	args = append(args, id)

	res, err := DB.ExecContext(ctx, query, args...)
	if err != nil {
		return err
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return sql.ErrNoRows
	}
	return nil
}

func (s *ProjectStore) Delete(ctx context.Context, id string) error {
	// Clear denormalized project_id on channels first (SQLite FK cascade
	// handles junction tables, but ON DELETE SET NULL requires PRAGMA
	// foreign_keys = ON which may not cascade reliably on all builds).
	DB.ExecContext(ctx, `UPDATE channels SET project_id = NULL WHERE project_id = ?`, id)

	res, err := DB.ExecContext(ctx, "DELETE FROM projects WHERE id = ?", id)
	if err != nil {
		return err
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return sql.ErrNoRows
	}
	return nil
}

// ── Listing ─────────────────────────────────

func (s *ProjectStore) ListForUser(ctx context.Context, userID string, teamIDs []string, includeArchived bool) ([]models.Project, error) {
	q := `
		SELECT p.id, p.name, p.description, p.color, p.icon, p.scope,
		       p.owner_id, p.team_id, p.is_archived, p.workspace_id, p.settings,
		       p.created_at, p.updated_at,
		       (SELECT COUNT(*) FROM project_channels WHERE project_id = p.id),
		       (SELECT COUNT(*) FROM project_knowledge_bases WHERE project_id = p.id),
		       (SELECT COUNT(*) FROM project_notes WHERE project_id = p.id)
		FROM projects p
		WHERE (
			(p.scope = 'personal' AND p.owner_id = ?)
			OR p.scope = 'global'`

	args := []interface{}{userID}

	for _, tid := range teamIDs {
		q += ` OR (p.scope = 'team' AND p.team_id = ?)`
		args = append(args, tid)
	}
	q += `)`

	if !includeArchived {
		q += ` AND p.is_archived = 0`
	}
	q += ` ORDER BY p.name`

	return s.queryProjects(ctx, q, args...)
}

// ── Channel Association ─────────────────────

func (s *ProjectStore) AddChannel(ctx context.Context, projectID, channelID string, position int) error {
	tx, err := DB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	// Remove existing (atomic move)
	tx.ExecContext(ctx, `DELETE FROM project_channels WHERE channel_id = ?`, channelID)

	_, err = tx.ExecContext(ctx, `
		INSERT INTO project_channels (project_id, channel_id, position)
		VALUES (?, ?, ?)`,
		projectID, channelID, position)
	if err != nil {
		return err
	}

	_, err = tx.ExecContext(ctx, `UPDATE channels SET project_id = ? WHERE id = ?`,
		projectID, channelID)
	if err != nil {
		return err
	}

	return tx.Commit()
}

func (s *ProjectStore) RemoveChannel(ctx context.Context, projectID, channelID string) error {
	tx, err := DB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	res, err := tx.ExecContext(ctx, `
		DELETE FROM project_channels WHERE project_id = ? AND channel_id = ?`,
		projectID, channelID)
	if err != nil {
		return err
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return sql.ErrNoRows
	}

	_, err = tx.ExecContext(ctx, `UPDATE channels SET project_id = NULL WHERE id = ?`, channelID)
	if err != nil {
		return err
	}

	return tx.Commit()
}

func (s *ProjectStore) ListChannels(ctx context.Context, projectID string) ([]models.ProjectChannel, error) {
	rows, err := DB.QueryContext(ctx, `
		SELECT project_id, channel_id, position, COALESCE(folder, ''), added_at
		FROM project_channels
		WHERE project_id = ?
		ORDER BY position, added_at`, projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []models.ProjectChannel
	for rows.Next() {
		var pc models.ProjectChannel
		if err := rows.Scan(&pc.ProjectID, &pc.ChannelID, &pc.Position, &pc.Folder, &pc.AddedAt); err != nil {
			return nil, err
		}
		result = append(result, pc)
	}
	return result, rows.Err()
}

func (s *ProjectStore) ReorderChannels(ctx context.Context, projectID string, channelIDs []string) error {
	tx, err := DB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	for i, chID := range channelIDs {
		_, err := tx.ExecContext(ctx, `
			UPDATE project_channels SET position = ?
			WHERE project_id = ? AND channel_id = ?`,
			i, projectID, chID)
		if err != nil {
			return err
		}
	}
	return tx.Commit()
}

// ── KB Association ──────────────────────────

func (s *ProjectStore) AddKB(ctx context.Context, projectID, kbID string, autoSearch bool) error {
	_, err := DB.ExecContext(ctx, `
		INSERT INTO project_knowledge_bases (project_id, kb_id, auto_search)
		VALUES (?, ?, ?)
		ON CONFLICT (project_id, kb_id) DO UPDATE SET auto_search = excluded.auto_search`,
		projectID, kbID, boolToInt(autoSearch))
	return err
}

func (s *ProjectStore) RemoveKB(ctx context.Context, projectID, kbID string) error {
	res, err := DB.ExecContext(ctx, `
		DELETE FROM project_knowledge_bases WHERE project_id = ? AND kb_id = ?`,
		projectID, kbID)
	if err != nil {
		return err
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return sql.ErrNoRows
	}
	return nil
}

func (s *ProjectStore) ListKBs(ctx context.Context, projectID string) ([]models.ProjectKB, error) {
	rows, err := DB.QueryContext(ctx, `
		SELECT pk.project_id, pk.kb_id, pk.auto_search, pk.added_at,
		       COALESCE(kb.name, '') AS name
		FROM project_knowledge_bases pk
		LEFT JOIN knowledge_bases kb ON kb.id = pk.kb_id
		WHERE pk.project_id = ?
		ORDER BY pk.added_at`, projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []models.ProjectKB
	for rows.Next() {
		var pkb models.ProjectKB
		var autoSearch int
		if err := rows.Scan(&pkb.ProjectID, &pkb.KBID, &autoSearch, &pkb.AddedAt, &pkb.Name); err != nil {
			return nil, err
		}
		pkb.AutoSearch = autoSearch != 0
		result = append(result, pkb)
	}
	return result, rows.Err()
}

func (s *ProjectStore) GetKBIDs(ctx context.Context, projectID string) ([]string, error) {
	rows, err := DB.QueryContext(ctx, `
		SELECT kb_id FROM project_knowledge_bases WHERE project_id = ?`, projectID)
	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)
	}
	return ids, rows.Err()
}

// ── Note Association ────────────────────────

func (s *ProjectStore) AddNote(ctx context.Context, projectID, noteID string) error {
	_, err := DB.ExecContext(ctx, `
		INSERT INTO project_notes (project_id, note_id)
		VALUES (?, ?)
		ON CONFLICT DO NOTHING`,
		projectID, noteID)
	return err
}

func (s *ProjectStore) RemoveNote(ctx context.Context, projectID, noteID string) error {
	res, err := DB.ExecContext(ctx, `
		DELETE FROM project_notes WHERE project_id = ? AND note_id = ?`,
		projectID, noteID)
	if err != nil {
		return err
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return sql.ErrNoRows
	}
	return nil
}

func (s *ProjectStore) ListNotes(ctx context.Context, projectID string) ([]models.ProjectNote, error) {
	rows, err := DB.QueryContext(ctx, `
		SELECT pn.project_id, pn.note_id, pn.added_at,
		       COALESCE(n.title, '') AS title
		FROM project_notes pn
		LEFT JOIN notes n ON n.id = pn.note_id
		WHERE pn.project_id = ?
		ORDER BY pn.added_at`, projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var result []models.ProjectNote
	for rows.Next() {
		var pn models.ProjectNote
		if err := rows.Scan(&pn.ProjectID, &pn.NoteID, &pn.AddedAt, &pn.Title); err != nil {
			return nil, err
		}
		result = append(result, pn)
	}
	return result, rows.Err()
}

// ── Access Check ────────────────────────────

func (s *ProjectStore) UserCanAccess(ctx context.Context, userID, projectID string, teamIDs []string) (bool, error) {
	q := `
		SELECT EXISTS(
			SELECT 1 FROM projects p
			WHERE p.id = ? AND (
				(p.scope = 'personal' AND p.owner_id = ?)
				OR p.scope = 'global'`

	args := []interface{}{projectID, userID}

	for _, tid := range teamIDs {
		q += ` OR (p.scope = 'team' AND p.team_id = ?)`
		args = append(args, tid)
	}
	q += `))`

	var ok bool
	err := DB.QueryRowContext(ctx, q, args...).Scan(&ok)
	return ok, err
}

func (s *ProjectStore) GetProjectIDForChannel(ctx context.Context, channelID string) (string, error) {
	var projectID string
	err := DB.QueryRowContext(ctx, `
		SELECT project_id FROM project_channels WHERE channel_id = ?`,
		channelID).Scan(&projectID)
	if err == sql.ErrNoRows {
		return "", nil
	}
	return projectID, err
}

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

func (s *ProjectStore) AdminList(ctx context.Context, includeArchived bool) ([]store.AdminProject, error) {
	archivedFilter := "AND p.is_archived = 0"
	if includeArchived {
		archivedFilter = ""
	}

	rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
		SELECT p.id, p.name, p.description, p.scope,
		       p.owner_id, p.team_id, p.is_archived,
		       p.created_at, p.updated_at,
		       (SELECT COUNT(*) FROM project_channels WHERE project_id = p.id),
		       (SELECT COUNT(*) FROM project_knowledge_bases WHERE project_id = p.id),
		       (SELECT COUNT(*) FROM project_notes WHERE project_id = p.id),
		       COALESCE(u.username, '')
		FROM projects p
		LEFT JOIN users u ON u.id = p.owner_id
		WHERE 1=1 %s
		ORDER BY p.updated_at DESC`, archivedFilter))
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	results := make([]store.AdminProject, 0)
	for rows.Next() {
		var p store.AdminProject
		var teamID sql.NullString
		var archived int
		if err := rows.Scan(
			&p.ID, &p.Name, &p.Description, &p.Scope,
			&p.OwnerID, &teamID, &archived,
			st(&p.CreatedAt), st(&p.UpdatedAt),
			&p.ChannelCount, &p.KBCount, &p.NoteCount,
			&p.OwnerName,
		); err != nil {
			return nil, err
		}
		p.IsArchived = archived != 0
		p.TeamID = NullableStringPtr(teamID)
		results = append(results, p)
	}
	return results, rows.Err()
}

// ── Helpers ─────────────────────────────────

func boolToInt(b bool) int {
	if b {
		return 1
	}
	return 0
}

func (s *ProjectStore) queryProjects(ctx context.Context, q string, args ...interface{}) ([]models.Project, error) {
	rows, err := DB.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, fmt.Errorf("queryProjects: %w", err)
	}
	defer rows.Close()

	var result []models.Project
	for rows.Next() {
		var p models.Project
		var teamID, workspaceID sql.NullString
		var archived int
		if err := rows.Scan(
			&p.ID, &p.Name, &p.Description, &p.Color, &p.Icon, &p.Scope,
			&p.OwnerID, &teamID, &archived, &workspaceID, &p.Settings,
			st(&p.CreatedAt), st(&p.UpdatedAt),
			&p.ChannelCount, &p.KBCount, &p.NoteCount,
		); err != nil {
			return nil, err
		}
		p.IsArchived = archived != 0
		p.TeamID = NullableStringPtr(teamID)
		p.WorkspaceID = NullableStringPtr(workspaceID)
		result = append(result, p)
	}
	return result, rows.Err()
}
