package services

// PartnerUnitService handles paliad.partner_units + paliad.partner_unit_members
// — the structural partner-led units (legacy "Dezernat"). Orthogonal to
// project teams: a user typically belongs to exactly one PartnerUnit but may
// work on projects across all of them.
//
// Every mutation emits a row into paliad.partner_unit_events in the same tx
// as the originating change so the global audit timeline (audit_service.go)
// can render the full history. The unit name is snapshotted into the event
// row so 'deleted' rows stay readable after the FK ON DELETE SET NULL fires.

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"
	"mgit.msbls.de/m/paliad/internal/offices"
)

// PartnerUnitService reads and writes paliad.partner_units.
type PartnerUnitService struct {
	db    *sqlx.DB
	users *UserService
}

// NewPartnerUnitService wires the service.
func NewPartnerUnitService(db *sqlx.DB, users *UserService) *PartnerUnitService {
	return &PartnerUnitService{db: db, users: users}
}

// CreatePartnerUnitInput is the payload for Create.
type CreatePartnerUnitInput struct {
	Name       string     `json:"name"`
	LeadUserID *uuid.UUID `json:"lead_user_id,omitempty"`
	Office     string     `json:"office"`
}

// UpdatePartnerUnitInput is the partial-update payload.
type UpdatePartnerUnitInput struct {
	Name       *string    `json:"name,omitempty"`
	LeadUserID *uuid.UUID `json:"lead_user_id,omitempty"`
	Office     *string    `json:"office,omitempty"`
}

// List returns every PartnerUnit (readable by any authenticated user — see RLS).
func (s *PartnerUnitService) List(ctx context.Context) ([]models.PartnerUnit, error) {
	rows := []models.PartnerUnit{}
	err := s.db.SelectContext(ctx, &rows,
		`SELECT id, name, lead_user_id, office, created_at, updated_at
		   FROM paliad.partner_units
		  ORDER BY office, name`)
	if err != nil {
		return nil, fmt.Errorf("list partner_units: %w", err)
	}
	return rows, nil
}

// GetByID returns one PartnerUnit or (nil, sql.ErrNoRows).
func (s *PartnerUnitService) GetByID(ctx context.Context, id uuid.UUID) (*models.PartnerUnit, error) {
	var d models.PartnerUnit
	err := s.db.GetContext(ctx, &d,
		`SELECT id, name, lead_user_id, office, created_at, updated_at
		   FROM paliad.partner_units WHERE id = $1`, id)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, sql.ErrNoRows
	}
	if err != nil {
		return nil, fmt.Errorf("get partner_unit: %w", err)
	}
	return &d, nil
}

// Create inserts a PartnerUnit. Admin-only. Emits a 'created' audit event
// in the same tx.
func (s *PartnerUnitService) Create(ctx context.Context, callerID uuid.UUID, input CreatePartnerUnitInput) (*models.PartnerUnit, error) {
	if err := s.requireAdmin(ctx, callerID); err != nil {
		return nil, err
	}
	if strings.TrimSpace(input.Name) == "" {
		return nil, fmt.Errorf("%w: name is required", ErrInvalidInput)
	}
	if !offices.IsValid(input.Office) {
		return nil, fmt.Errorf("%w: invalid office %q", ErrInvalidInput, input.Office)
	}
	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() //nolint:errcheck

	if _, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.partner_units (id, name, lead_user_id, office, created_at, updated_at)
		 VALUES ($1, $2, $3, $4, $5, $5)`,
		id, input.Name, input.LeadUserID, input.Office, now); err != nil {
		return nil, fmt.Errorf("insert partner_unit: %w", err)
	}

	if err := s.emit(ctx, tx, callerID, &id, input.Name, "created", map[string]any{
		"name":         input.Name,
		"office":       input.Office,
		"lead_user_id": input.LeadUserID,
	}); err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit tx: %w", err)
	}
	return s.GetByID(ctx, id)
}

// Update applies a partial update. Admin-only. Emits an 'updated' event with
// before/after snapshots in the same tx.
func (s *PartnerUnitService) Update(ctx context.Context, callerID, id uuid.UUID, input UpdatePartnerUnitInput) (*models.PartnerUnit, error) {
	if err := s.requireAdmin(ctx, callerID); err != nil {
		return nil, err
	}
	current, err := s.GetByID(ctx, 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++
	}
	before := map[string]any{}
	after := map[string]any{}
	fields := []string{}
	if input.Name != nil && *input.Name != current.Name {
		appendSet("name", *input.Name)
		before["name"] = current.Name
		after["name"] = *input.Name
		fields = append(fields, "name")
	}
	if input.LeadUserID != nil {
		curLead := uuid.Nil
		if current.LeadUserID != nil {
			curLead = *current.LeadUserID
		}
		if *input.LeadUserID != curLead {
			appendSet("lead_user_id", *input.LeadUserID)
			before["lead_user_id"] = current.LeadUserID
			after["lead_user_id"] = *input.LeadUserID
			fields = append(fields, "lead_user_id")
		}
	}
	if input.Office != nil && *input.Office != current.Office {
		if !offices.IsValid(*input.Office) {
			return nil, fmt.Errorf("%w: invalid office %q", ErrInvalidInput, *input.Office)
		}
		appendSet("office", *input.Office)
		before["office"] = current.Office
		after["office"] = *input.Office
		fields = append(fields, "office")
	}
	if len(sets) == 0 {
		return current, nil
	}
	appendSet("updated_at", time.Now().UTC())
	args = append(args, id)
	query := fmt.Sprintf("UPDATE paliad.partner_units 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() //nolint:errcheck

	if _, err := tx.ExecContext(ctx, query, args...); err != nil {
		return nil, fmt.Errorf("update partner_unit: %w", err)
	}
	if err := s.emit(ctx, tx, callerID, &id, current.Name, "updated", map[string]any{
		"before": before,
		"after":  after,
		"fields": fields,
	}); err != nil {
		return nil, err
	}
	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit tx: %w", err)
	}
	return s.GetByID(ctx, id)
}

// Delete removes a PartnerUnit (cascades memberships). Admin-only. Emits a
// 'deleted' audit event in the same tx — the FK on partner_unit_events has
// ON DELETE SET NULL so the historical row survives the cascade.
func (s *PartnerUnitService) Delete(ctx context.Context, callerID, id uuid.UUID) error {
	if err := s.requireAdmin(ctx, callerID); err != nil {
		return err
	}
	current, err := s.GetByID(ctx, id)
	if err != nil {
		return err
	}

	var memberCount int
	if err := s.db.GetContext(ctx, &memberCount,
		`SELECT COUNT(*) FROM paliad.partner_unit_members WHERE partner_unit_id = $1`, id); err != nil {
		return fmt.Errorf("count members: %w", err)
	}

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

	// Emit BEFORE delete so the FK still resolves; ON DELETE SET NULL fires
	// on cascade and clears partner_unit_id while keeping unit_name + payload.
	if err := s.emit(ctx, tx, callerID, &id, current.Name, "deleted", map[string]any{
		"name":         current.Name,
		"office":       current.Office,
		"lead_user_id": current.LeadUserID,
		"member_count": memberCount,
	}); err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx, `DELETE FROM paliad.partner_units WHERE id = $1`, id); err != nil {
		return fmt.Errorf("delete partner_unit: %w", err)
	}
	if err := tx.Commit(); err != nil {
		return fmt.Errorf("commit tx: %w", err)
	}
	return nil
}

// AddMember inserts a (partner_unit, user) membership. Admin-only. Idempotent.
// Emits 'member_added' only when a row is actually inserted.
func (s *PartnerUnitService) AddMember(ctx context.Context, callerID, partnerUnitID, userID uuid.UUID) error {
	if err := s.requireAdmin(ctx, callerID); err != nil {
		return err
	}
	unit, err := s.GetByID(ctx, partnerUnitID)
	if err != nil {
		return err
	}

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

	res, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.partner_unit_members (partner_unit_id, user_id, created_at)
		 VALUES ($1, $2, now()) ON CONFLICT (partner_unit_id, user_id) DO NOTHING`,
		partnerUnitID, userID)
	if err != nil {
		return fmt.Errorf("add partner_unit member: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return tx.Commit()
	}

	var disp struct {
		DN string `db:"display_name"`
		Em string `db:"email"`
	}
	_ = s.db.GetContext(ctx, &disp,
		`SELECT display_name, email FROM paliad.users WHERE id = $1`, userID)

	if err := s.emit(ctx, tx, callerID, &partnerUnitID, unit.Name, "member_added", map[string]any{
		"user_id":      userID,
		"display_name": disp.DN,
		"email":        disp.Em,
	}); err != nil {
		return err
	}
	return tx.Commit()
}

// RemoveMember deletes a (partner_unit, user) membership. Admin-only.
// Emits 'member_removed' only when a row is actually deleted.
func (s *PartnerUnitService) RemoveMember(ctx context.Context, callerID, partnerUnitID, userID uuid.UUID) error {
	if err := s.requireAdmin(ctx, callerID); err != nil {
		return err
	}
	unit, err := s.GetByID(ctx, partnerUnitID)
	if err != nil {
		return err
	}

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

	res, err := tx.ExecContext(ctx,
		`DELETE FROM paliad.partner_unit_members WHERE partner_unit_id = $1 AND user_id = $2`,
		partnerUnitID, userID)
	if err != nil {
		return fmt.Errorf("remove partner_unit member: %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return tx.Commit()
	}

	if err := s.emit(ctx, tx, callerID, &partnerUnitID, unit.Name, "member_removed", map[string]any{
		"user_id": userID,
	}); err != nil {
		return err
	}
	return tx.Commit()
}

// SetMemberRole updates the unit_role column on a (partner_unit, user)
// membership. Admin-only. Validates the role against the migration-055 CHECK.
// Emits 'member_role_changed' carrying the prior + new role values so the
// audit trail captures the transition.
func (s *PartnerUnitService) SetMemberRole(ctx context.Context, callerID, partnerUnitID, userID uuid.UUID, role string) error {
	if err := s.requireAdmin(ctx, callerID); err != nil {
		return err
	}
	if !isValidUnitRole(role) {
		return fmt.Errorf("%w: invalid unit_role %q", ErrInvalidInput, role)
	}
	unit, err := s.GetByID(ctx, partnerUnitID)
	if err != nil {
		return err
	}

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

	var prior string
	err = tx.GetContext(ctx, &prior,
		`SELECT unit_role FROM paliad.partner_unit_members
		  WHERE partner_unit_id = $1 AND user_id = $2`,
		partnerUnitID, userID)
	if err != nil {
		return fmt.Errorf("read prior unit_role: %w", err)
	}
	if prior == role {
		return tx.Commit()
	}

	if _, err := tx.ExecContext(ctx,
		`UPDATE paliad.partner_unit_members
		    SET unit_role = $3
		  WHERE partner_unit_id = $1 AND user_id = $2`,
		partnerUnitID, userID, role); err != nil {
		return fmt.Errorf("update unit_role: %w", err)
	}

	if err := s.emit(ctx, tx, callerID, &partnerUnitID, unit.Name, "member_role_changed", map[string]any{
		"user_id":   userID,
		"old_role":  prior,
		"new_role":  role,
	}); err != nil {
		return err
	}
	return tx.Commit()
}

// AddMemberTx is the same as AddMember but runs inside the caller's tx and
// skips the admin gate (caller has already authorised the parent operation).
// Used by user_service.OnboardUser to insert a partner_unit membership in
// the same tx as the user-create.
func (s *PartnerUnitService) AddMemberTx(ctx context.Context, tx *sqlx.Tx, actorID, partnerUnitID, userID uuid.UUID) error {
	res, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.partner_unit_members (partner_unit_id, user_id, created_at)
		 VALUES ($1, $2, now()) ON CONFLICT (partner_unit_id, user_id) DO NOTHING`,
		partnerUnitID, userID)
	if err != nil {
		return fmt.Errorf("add partner_unit member (tx): %w", err)
	}
	n, _ := res.RowsAffected()
	if n == 0 {
		return nil
	}
	var unitName string
	if err := tx.GetContext(ctx, &unitName,
		`SELECT name FROM paliad.partner_units WHERE id = $1`, partnerUnitID); err != nil {
		return fmt.Errorf("lookup partner_unit name: %w", err)
	}
	var disp struct {
		DN string `db:"display_name"`
		Em string `db:"email"`
	}
	_ = tx.GetContext(ctx, &disp,
		`SELECT display_name, email FROM paliad.users WHERE id = $1`, userID)

	return s.emit(ctx, tx, actorID, &partnerUnitID, unitName, "member_added", map[string]any{
		"user_id":      userID,
		"display_name": disp.DN,
		"email":        disp.Em,
		"source":       "onboarding",
	})
}

// PartnerUnitMemberDetail is one user's membership row enriched with display
// fields for the admin/team UIs.
//
// UnitRole (added by t-paliad-139 / migration 055) is the per-unit role
// distinction used by the derivation rule: a unit attached to a project
// auto-derives its members whose unit_role is in the attachment's
// derive_unit_roles set (default {pa, senior_pa}). Possible values:
// 'lead' | 'attorney' | 'senior_pa' | 'pa' | 'paralegal'. Defaults to
// 'attorney' for every pre-055 row.
type PartnerUnitMemberDetail struct {
	UserID      uuid.UUID `db:"user_id"      json:"user_id"`
	Email       string    `db:"email"        json:"email"`
	DisplayName string    `db:"display_name" json:"display_name"`
	Office      string    `db:"office"       json:"office"`
	JobTitle    *string   `db:"job_title"    json:"job_title"`
	UnitRole    string    `db:"unit_role"    json:"unit_role"`
	CreatedAt   time.Time `db:"created_at"   json:"created_at"`
}

// PartnerUnitMemberRole values (mirror migration 055 CHECK constraint).
const (
	UnitRoleLead      = "lead"
	UnitRoleAttorney  = "attorney"
	UnitRoleSeniorPA  = "senior_pa"
	UnitRolePA        = "pa"
	UnitRoleParalegal = "paralegal"
)

func isValidUnitRole(r string) bool {
	switch r {
	case UnitRoleLead, UnitRoleAttorney, UnitRoleSeniorPA, UnitRolePA, UnitRoleParalegal:
		return true
	}
	return false
}

// ListMembers returns users in the PartnerUnit, enriched with display fields.
//
// INNER JOIN on paliad.users: partner_unit_members.user_id FKs auth.users, so
// pre-onboarding members (auth row exists, paliad.users row doesn't) would
// otherwise produce NULL display_name/office and break the scan.
// Skipping them is the right UX — without an onboarded profile there's
// nothing meaningful to render.
func (s *PartnerUnitService) ListMembers(ctx context.Context, partnerUnitID uuid.UUID) ([]PartnerUnitMemberDetail, error) {
	var rows []PartnerUnitMemberDetail
	err := s.db.SelectContext(ctx, &rows,
		`SELECT pum.user_id, pum.created_at, pum.unit_role,
		        u.email, u.display_name, u.office, u.job_title
		   FROM paliad.partner_unit_members pum
		   JOIN paliad.users u ON u.id = pum.user_id
		  WHERE pum.partner_unit_id = $1
		  ORDER BY u.display_name`, partnerUnitID)
	if err != nil {
		return nil, fmt.Errorf("list partner_unit members: %w", err)
	}
	return rows, nil
}

// PartnerUnitWithMembers is a unit row enriched with its lead user
// snapshot and full member list. Used by the /team directory page so the
// frontend can render the "by partner unit" grouping with one fetch.
type PartnerUnitWithMembers struct {
	models.PartnerUnit
	LeadDisplayName *string                   `json:"lead_display_name,omitempty"`
	LeadEmail       *string                   `json:"lead_email,omitempty"`
	Members         []PartnerUnitMemberDetail `json:"members"`
}

// ListWithMembers returns every PartnerUnit enriched with its lead's display
// name + email and the full members list. Two short queries (one per
// table) are joined in Go to avoid a Cartesian explosion when units have
// many members.
func (s *PartnerUnitService) ListWithMembers(ctx context.Context) ([]PartnerUnitWithMembers, error) {
	type unitRow struct {
		models.PartnerUnit
		LeadDisplayName *string `db:"lead_display_name"`
		LeadEmail       *string `db:"lead_email"`
	}
	var units []unitRow
	err := s.db.SelectContext(ctx, &units,
		`SELECT pu.id, pu.name, pu.lead_user_id, pu.office, pu.created_at, pu.updated_at,
		        lu.display_name AS lead_display_name,
		        lu.email        AS lead_email
		   FROM paliad.partner_units pu
		   LEFT JOIN paliad.users lu ON lu.id = pu.lead_user_id
		  ORDER BY pu.office, pu.name`)
	if err != nil {
		return nil, fmt.Errorf("list partner_units: %w", err)
	}

	type memberRow struct {
		PartnerUnitMemberDetail
		PartnerUnitID uuid.UUID `db:"partner_unit_id"`
	}
	var members []memberRow
	err = s.db.SelectContext(ctx, &members,
		`SELECT pum.partner_unit_id, pum.user_id, pum.created_at, pum.unit_role,
		        u.email, u.display_name, u.office, u.job_title
		   FROM paliad.partner_unit_members pum
		   JOIN paliad.users u ON u.id = pum.user_id
		  ORDER BY u.display_name`)
	if err != nil {
		return nil, fmt.Errorf("list partner_unit members: %w", err)
	}

	byUnit := map[uuid.UUID][]PartnerUnitMemberDetail{}
	for _, m := range members {
		byUnit[m.PartnerUnitID] = append(byUnit[m.PartnerUnitID], m.PartnerUnitMemberDetail)
	}

	out := make([]PartnerUnitWithMembers, len(units))
	for i, u := range units {
		out[i] = PartnerUnitWithMembers{
			PartnerUnit:     u.PartnerUnit,
			LeadDisplayName: u.LeadDisplayName,
			LeadEmail:       u.LeadEmail,
			Members:         byUnit[u.ID],
		}
		if out[i].Members == nil {
			out[i].Members = []PartnerUnitMemberDetail{}
		}
	}
	return out, nil
}

// GetMembership returns the user's PartnerUnit memberships (zero or more).
// Used by the settings page to render the user's own partner unit card.
func (s *PartnerUnitService) GetMembership(ctx context.Context, userID uuid.UUID) ([]models.PartnerUnit, error) {
	rows := []models.PartnerUnit{}
	err := s.db.SelectContext(ctx, &rows,
		`SELECT pu.id, pu.name, pu.lead_user_id, pu.office, pu.created_at, pu.updated_at
		   FROM paliad.partner_units pu
		   JOIN paliad.partner_unit_members pum ON pum.partner_unit_id = pu.id
		  WHERE pum.user_id = $1
		  ORDER BY pu.name`, userID)
	if err != nil {
		return nil, fmt.Errorf("get user partner_unit memberships: %w", err)
	}
	return rows, nil
}

// ---------------------------------------------------------------------------

func (s *PartnerUnitService) requireAdmin(ctx context.Context, userID uuid.UUID) error {
	u, err := s.users.GetByID(ctx, userID)
	if err != nil {
		return err
	}
	if u == nil || u.GlobalRole != "global_admin" {
		return fmt.Errorf("%w: global admin required", ErrForbidden)
	}
	return nil
}

// emit writes one audit row to paliad.partner_unit_events inside the caller's
// tx. unitName is snapshotted so deleted units stay readable in the timeline
// (their FK is cleared to NULL by ON DELETE SET NULL after the unit row is
// gone).
func (s *PartnerUnitService) emit(ctx context.Context, tx *sqlx.Tx, actorID uuid.UUID,
	unitID *uuid.UUID, unitName, eventType string, payload any) error {
	p, err := json.Marshal(payload)
	if err != nil {
		return fmt.Errorf("marshal audit payload: %w", err)
	}
	if _, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.partner_unit_events
		   (partner_unit_id, actor_id, event_type, unit_name, payload)
		 VALUES ($1, $2, $3, $4, $5)`,
		unitID, actorID, eventType, unitName, p); err != nil {
		return fmt.Errorf("emit partner_unit event %q: %w", eventType, err)
	}
	return nil
}
