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/models"
)

// PartyService reads and writes paliad.parties. Visibility inherits from
// the parent Project.
type PartyService struct {
	db       *sqlx.DB
	projects *ProjectService
}

// NewPartyService wires the service.
func NewPartyService(db *sqlx.DB, projects *ProjectService) *PartyService {
	return &PartyService{db: db, projects: projects}
}

const partyColumns = `id, project_id, name, role, representative, contact_info,
	created_at, updated_at`

// CreatePartyInput is the payload for Create.
type CreatePartyInput struct {
	Name           string          `json:"name"`
	Role           *string         `json:"role,omitempty"`
	Representative *string         `json:"representative,omitempty"`
	ContactInfo    json.RawMessage `json:"contact_info,omitempty"`
}

// PartySearchHit is one row of the cross-project party search — a real
// paliad.parties row enriched with the parent project's title and
// reference so the picker can render context the lawyer needs to
// disambiguate identically-named parties on different cases
// (t-paliad-287).
type PartySearchHit struct {
	ID               uuid.UUID `db:"id"                json:"id"`
	ProjectID        uuid.UUID `db:"project_id"        json:"project_id"`
	ProjectTitle     string    `db:"project_title"     json:"project_title"`
	ProjectReference *string   `db:"project_reference" json:"project_reference,omitempty"`
	Name             string    `db:"name"              json:"name"`
	Role             *string   `db:"role"              json:"role,omitempty"`
	Representative   *string   `db:"representative"    json:"representative,omitempty"`
}

// Search returns parties from every project the caller can see, matched
// by case-insensitive substring on name OR representative. Empty query
// returns the 20 most recently-updated parties so the picker isn't
// blank on first open. Capped at 25 rows; the frontend doesn't paginate
// (the typical PA looks for one party they remember by name, not browses).
//
// Visibility is enforced inline via visibilityPredicatePositional —
// invisible projects' parties never surface in the result set.
func (s *PartyService) Search(ctx context.Context, userID uuid.UUID, query string, limit int) ([]PartySearchHit, error) {
	if limit <= 0 || limit > 50 {
		limit = 25
	}
	q := strings.TrimSpace(query)
	args := []any{userID}
	conds := []string{visibilityPredicatePositional("p", 1)}
	if q != "" {
		args = append(args, "%"+q+"%")
		conds = append(conds,
			fmt.Sprintf(`(pa.name ILIKE $%d OR COALESCE(pa.representative,'') ILIKE $%d)`,
				len(args), len(args)))
	}
	args = append(args, limit)
	sqlStr := `
		SELECT pa.id, pa.project_id, p.title AS project_title,
		       p.reference AS project_reference,
		       pa.name, pa.role, pa.representative
		  FROM paliad.parties pa
		  JOIN paliad.projects p ON p.id = pa.project_id
		 WHERE ` + strings.Join(conds, " AND ") + `
		 ORDER BY pa.updated_at DESC
		 LIMIT $` + fmt.Sprintf("%d", len(args))
	hits := []PartySearchHit{}
	if err := s.db.SelectContext(ctx, &hits, sqlStr, args...); err != nil {
		return nil, fmt.Errorf("search parties: %w", err)
	}
	return hits, nil
}

// ListForProject returns all Parties for the Project, visibility-checked.
func (s *PartyService) ListForProject(ctx context.Context, userID, projectID uuid.UUID) ([]models.Party, error) {
	if _, err := s.projects.GetByID(ctx, userID, projectID); err != nil {
		return nil, err
	}
	rows := []models.Party{}
	if err := s.db.SelectContext(ctx, &rows,
		`SELECT `+partyColumns+`
		   FROM paliad.parties
		  WHERE project_id = $1
		  ORDER BY name`, projectID); err != nil {
		return nil, fmt.Errorf("list parties: %w", err)
	}
	return rows, nil
}

// Create inserts a Party under a Project; visibility is checked on the parent.
func (s *PartyService) Create(ctx context.Context, userID, projectID uuid.UUID, input CreatePartyInput) (*models.Party, error) {
	if strings.TrimSpace(input.Name) == "" {
		return nil, fmt.Errorf("%w: name is required", ErrInvalidInput)
	}
	if _, err := s.projects.GetByID(ctx, userID, projectID); err != nil {
		return nil, err
	}

	contact := input.ContactInfo
	if len(contact) == 0 {
		contact = json.RawMessage(`{}`)
	}

	id := uuid.New()
	now := time.Now().UTC()
	if _, err := s.db.ExecContext(ctx,
		`INSERT INTO paliad.parties
		   (id, project_id, name, role, representative, contact_info,
		    created_at, updated_at)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $7)`,
		id, projectID, input.Name, input.Role, input.Representative, contact, now,
	); err != nil {
		return nil, fmt.Errorf("insert party: %w", err)
	}

	var p models.Party
	if err := s.db.GetContext(ctx, &p,
		`SELECT `+partyColumns+` FROM paliad.parties WHERE id = $1`, id); err != nil {
		return nil, fmt.Errorf("fetch created party: %w", err)
	}
	return &p, nil
}

// Delete removes a Party. Partner/admin only.
func (s *PartyService) Delete(ctx context.Context, userID, partyID uuid.UUID) error {
	user, err := s.projects.Users().GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if user == nil {
		return ErrNotVisible
	}
	if user.GlobalRole != "global_admin" {
		return fmt.Errorf("%w: only partners/admins can delete Parties", ErrForbidden)
	}

	var projectID uuid.UUID
	err = s.db.GetContext(ctx, &projectID,
		`SELECT project_id FROM paliad.parties WHERE id = $1`, partyID)
	if errors.Is(err, sql.ErrNoRows) {
		return ErrNotVisible
	}
	if err != nil {
		return fmt.Errorf("lookup party parent: %w", err)
	}
	if _, err := s.projects.GetByID(ctx, userID, projectID); err != nil {
		return err
	}
	if _, err := s.db.ExecContext(ctx,
		`DELETE FROM paliad.parties WHERE id = $1`, partyID); err != nil {
		return fmt.Errorf("delete party: %w", err)
	}
	return nil
}
