package services

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

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"

	"mgit.msbls.de/m/paliad/internal/checklists"
	"mgit.msbls.de/m/paliad/internal/models"
)

// ChecklistInstanceService reads and writes paliad.checklist_instances.
//
// Visibility mirrors paliad.appointments (project_id nullable):
//   - project_id NULL     → creator-only (personal instance)
//   - project_id NOT NULL → parent Project's team-based gate
type ChecklistInstanceService struct {
	db       *sqlx.DB
	projects *ProjectService
}

func NewChecklistInstanceService(db *sqlx.DB, projects *ProjectService) *ChecklistInstanceService {
	return &ChecklistInstanceService{db: db, projects: projects}
}

const checklistInstanceColumns = `ci.id, ci.template_slug, ci.name, ci.project_id, ci.state,
	ci.created_by, ci.created_at, ci.updated_at`

const checklistInstanceWithProjectSelect = `SELECT ` + checklistInstanceColumns + `,
	p.reference AS project_reference,
	p.title     AS project_title
	FROM paliad.checklist_instances ci
	LEFT JOIN paliad.projects p ON p.id = ci.project_id`

// CreateInstanceInput is the POST body for creating a new instance.
type CreateInstanceInput struct {
	Name      string     `json:"name"`
	ProjectID *uuid.UUID `json:"project_id,omitempty"`
}

// UpdateInstanceInput is the PATCH body. Any subset of fields may be set.
type UpdateInstanceInput struct {
	Name         *string         `json:"name,omitempty"`
	ProjectID    *uuid.UUID      `json:"project_id,omitempty"`
	State        map[string]bool `json:"state,omitempty"`
	ClearProject bool            `json:"clear_projekt,omitempty"`
}

// ListForTemplate returns every visible instance of a given template.
func (s *ChecklistInstanceService) ListForTemplate(ctx context.Context, userID uuid.UUID, slug string) ([]models.ChecklistInstanceWithProject, error) {
	if _, ok := checklists.Find(slug); !ok {
		return nil, fmt.Errorf("%w: unknown template slug %q", ErrInvalidInput, slug)
	}
	user, err := s.projects.Users().GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return []models.ChecklistInstanceWithProject{}, nil
	}
	query := checklistInstanceWithProjectSelect + `
		WHERE ci.template_slug = :slug
		  AND (
		    (ci.project_id IS NULL AND ci.created_by = :user_id)
		    OR (ci.project_id IS NOT NULL AND ` + visibilityPredicate("p") + `)
		  )
		ORDER BY ci.updated_at DESC`
	args := map[string]any{
		"slug":    slug,
		"user_id": userID,
	}
	return s.listWithProject(ctx, query, args)
}

// ListForProject returns every visible instance attached to a Project.
func (s *ChecklistInstanceService) ListForProject(ctx context.Context, userID, projectID uuid.UUID) ([]models.ChecklistInstanceWithProject, error) {
	if _, err := s.projects.GetByID(ctx, userID, projectID); err != nil {
		return nil, err
	}
	query := checklistInstanceWithProjectSelect + `
		WHERE ci.project_id = :project_id
		ORDER BY ci.updated_at DESC`
	return s.listWithProject(ctx, query, map[string]any{"project_id": projectID})
}

// ListAllVisible returns every checklist instance the user can see across
// all templates and projects. Personal instances (project_id NULL) are scoped
// to the creator; project-attached instances follow paliad.can_see_project.
// Ordered by created_at DESC so the most recently created instances surface
// first on the /checklists "Vorhandene Instanzen" tab.
func (s *ChecklistInstanceService) ListAllVisible(ctx context.Context, userID uuid.UUID) ([]models.ChecklistInstanceWithProject, error) {
	user, err := s.projects.Users().GetByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if user == nil {
		return []models.ChecklistInstanceWithProject{}, nil
	}
	query := checklistInstanceWithProjectSelect + `
		WHERE (
		    (ci.project_id IS NULL AND ci.created_by = :user_id)
		    OR (ci.project_id IS NOT NULL AND ` + visibilityPredicate("p") + `)
		  )
		ORDER BY ci.created_at DESC`
	return s.listWithProject(ctx, query, map[string]any{"user_id": userID})
}

// GetByID returns a single instance with visibility check applied.
func (s *ChecklistInstanceService) GetByID(ctx context.Context, userID, id uuid.UUID) (*models.ChecklistInstance, error) {
	inst, err := s.getByIDUnchecked(ctx, id)
	if err != nil {
		return nil, err
	}
	if err := s.requireVisible(ctx, userID, inst); err != nil {
		return nil, err
	}
	return inst, nil
}

// Create inserts a new instance.
func (s *ChecklistInstanceService) Create(ctx context.Context, userID uuid.UUID, slug string, input CreateInstanceInput) (*models.ChecklistInstance, error) {
	if _, ok := checklists.Find(slug); !ok {
		return nil, fmt.Errorf("%w: unknown template slug %q", ErrInvalidInput, slug)
	}
	name := strings.TrimSpace(input.Name)
	if name == "" {
		return nil, fmt.Errorf("%w: name is required", ErrInvalidInput)
	}
	if len(name) > 200 {
		return nil, fmt.Errorf("%w: name exceeds 200 characters", ErrInvalidInput)
	}
	if input.ProjectID != nil {
		if _, err := s.projects.GetByID(ctx, userID, *input.ProjectID); err != nil {
			return nil, err
		}
	}

	id := uuid.New()
	now := time.Now().UTC()

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("begin tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.checklist_instances
		   (id, template_slug, name, project_id, state, created_by, created_at, updated_at)
		 VALUES ($1, $2, $3, $4, '{}'::jsonb, $5, $6, $6)`,
		id, slug, name, input.ProjectID, userID, now,
	); err != nil {
		return nil, fmt.Errorf("insert checklist_instance: %w", err)
	}

	if input.ProjectID != nil {
		desc := fmt.Sprintf("Checkliste \u201E%s\u201C angelegt", name)
		descPtr := &desc
		if err := insertProjectEventWithMeta(ctx, tx, *input.ProjectID, userID,
			"checklist_created", "Checklist created", descPtr,
			map[string]any{"checklist_instance_id": id}); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit create instance: %w", err)
	}
	return s.GetByID(ctx, userID, id)
}

// Update applies a partial update (rename, re-link, state merge).
func (s *ChecklistInstanceService) Update(ctx context.Context, userID, id uuid.UUID, input UpdateInstanceInput) (*models.ChecklistInstance, error) {
	current, err := s.GetByID(ctx, userID, id)
	if err != nil {
		return nil, err
	}

	sets := []string{}
	args := []any{}
	next := 1
	appendSet := func(col string, val any) {
		sets = append(sets, fmt.Sprintf("%s = $%d", col, next))
		args = append(args, val)
		next++
	}

	var renamedTo *string
	if input.Name != nil {
		n := strings.TrimSpace(*input.Name)
		if n == "" {
			return nil, fmt.Errorf("%w: name cannot be empty", ErrInvalidInput)
		}
		if len(n) > 200 {
			return nil, fmt.Errorf("%w: name exceeds 200 characters", ErrInvalidInput)
		}
		appendSet("name", n)
		renamedTo = &n
	}

	var relinkTo *uuid.UUID
	var unlinking bool
	if input.ClearProject {
		appendSet("project_id", nil)
		unlinking = true
	} else if input.ProjectID != nil {
		if _, err := s.projects.GetByID(ctx, userID, *input.ProjectID); err != nil {
			return nil, err
		}
		appendSet("project_id", *input.ProjectID)
		relinkTo = input.ProjectID
	}

	if len(input.State) > 0 {
		patch, err := json.Marshal(input.State)
		if err != nil {
			return nil, fmt.Errorf("marshal state patch: %w", err)
		}
		sets = append(sets, fmt.Sprintf("state = state || $%d::jsonb", next))
		args = append(args, string(patch))
		next++
	}

	if len(sets) == 0 {
		return current, nil
	}
	appendSet("updated_at", time.Now().UTC())

	args = append(args, id)
	query := fmt.Sprintf("UPDATE paliad.checklist_instances SET %s WHERE id = $%d",
		strings.Join(sets, ", "), next)

	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("begin tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx, query, args...); err != nil {
		return nil, fmt.Errorf("update checklist_instance: %w", err)
	}

	meta := map[string]any{"checklist_instance_id": id}
	switch {
	case renamedTo != nil && current.ProjectID != nil:
		desc := fmt.Sprintf("Checkliste umbenannt: \u201E%s\u201C", *renamedTo)
		descPtr := &desc
		if err := insertProjectEventWithMeta(ctx, tx, *current.ProjectID, userID,
			"checklist_renamed", "Checklist renamed", descPtr, meta); err != nil {
			return nil, err
		}
	case unlinking && current.ProjectID != nil:
		desc := fmt.Sprintf("Checkliste \u201E%s\u201C von Project getrennt", current.Name)
		descPtr := &desc
		if err := insertProjectEventWithMeta(ctx, tx, *current.ProjectID, userID,
			"checklist_unlinked", "Checklist unlinked", descPtr, meta); err != nil {
			return nil, err
		}
	case relinkTo != nil:
		desc := fmt.Sprintf("Checkliste \u201E%s\u201C mit Project verknüpft", current.Name)
		descPtr := &desc
		if err := insertProjectEventWithMeta(ctx, tx, *relinkTo, userID,
			"checklist_linked", "Checklist linked", descPtr, meta); err != nil {
			return nil, err
		}
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit update instance: %w", err)
	}
	return s.GetByID(ctx, userID, id)
}

// Reset clears all checkbox state on an instance.
func (s *ChecklistInstanceService) Reset(ctx context.Context, userID, id uuid.UUID) (*models.ChecklistInstance, error) {
	current, err := s.GetByID(ctx, userID, id)
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("begin tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx,
		`UPDATE paliad.checklist_instances
		    SET state = '{}'::jsonb, updated_at = $1
		  WHERE id = $2`, now, id); err != nil {
		return nil, fmt.Errorf("reset instance: %w", err)
	}
	if current.ProjectID != nil {
		desc := fmt.Sprintf("Checkliste \u201E%s\u201C zurückgesetzt", current.Name)
		descPtr := &desc
		if err := insertProjectEventWithMeta(ctx, tx, *current.ProjectID, userID,
			"checklist_reset", "Checklist reset", descPtr,
			map[string]any{"checklist_instance_id": id}); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit reset instance: %w", err)
	}
	return s.GetByID(ctx, userID, id)
}

// Delete removes an instance. Creator or partner/admin.
func (s *ChecklistInstanceService) Delete(ctx context.Context, userID, id uuid.UUID) error {
	current, err := s.GetByID(ctx, userID, id)
	if err != nil {
		return err
	}
	if current.CreatedBy != userID {
		user, err := s.projects.Users().GetByID(ctx, userID)
		if err != nil {
			return err
		}
		if user.GlobalRole != "global_admin" {
			return fmt.Errorf("%w: only the creator or a partner/admin can delete a checklist instance", ErrForbidden)
		}
	}
	tx, err := s.db.BeginTxx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx,
		`DELETE FROM paliad.checklist_instances WHERE id = $1`, id); err != nil {
		return fmt.Errorf("delete instance: %w", err)
	}
	if current.ProjectID != nil {
		desc := fmt.Sprintf("Checkliste \u201E%s\u201C gelöscht", current.Name)
		descPtr := &desc
		if err := insertProjectEvent(ctx, tx, *current.ProjectID, userID,
			"checklist_deleted", "Checklist deleted", descPtr); err != nil {
			return err
		}
	}
	return tx.Commit()
}

// --- internals ------------------------------------------------------------

func (s *ChecklistInstanceService) listWithProject(ctx context.Context, query string, args map[string]any) ([]models.ChecklistInstanceWithProject, error) {
	stmt, err := s.db.PrepareNamedContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("prepare list instances: %w", err)
	}
	defer stmt.Close()

	rows := []models.ChecklistInstanceWithProject{}
	if err := stmt.SelectContext(ctx, &rows, args); err != nil {
		return nil, fmt.Errorf("list checklist_instances: %w", err)
	}
	return rows, nil
}

func (s *ChecklistInstanceService) getByIDUnchecked(ctx context.Context, id uuid.UUID) (*models.ChecklistInstance, error) {
	var inst models.ChecklistInstance
	err := s.db.GetContext(ctx, &inst,
		`SELECT id, template_slug, name, project_id, state, created_by, created_at, updated_at
		   FROM paliad.checklist_instances WHERE id = $1`, id)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotVisible
	}
	if err != nil {
		return nil, fmt.Errorf("fetch checklist_instance: %w", err)
	}
	return &inst, nil
}

func (s *ChecklistInstanceService) requireVisible(ctx context.Context, userID uuid.UUID, inst *models.ChecklistInstance) error {
	if inst.ProjectID == nil {
		if inst.CreatedBy != userID {
			return ErrNotVisible
		}
		return nil
	}
	_, err := s.projects.GetByID(ctx, userID, *inst.ProjectID)
	return err
}
