package services

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

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

	"mgit.msbls.de/m/paliad/internal/offices"
)

// ChecklistShareService is the write surface for paliad.checklist_shares
// (mig 115). Owners grant; owner-or-global_admin revokes. ListGrants is
// owner-only (returning all 4 recipient kinds) — recipients see "this
// is shared with me" only implicitly via the visibility predicate.
type ChecklistShareService struct {
	db        *sqlx.DB
	templates *ChecklistTemplateService
	audit     *SystemAuditLogService
	users     *UserService
}

func NewChecklistShareService(db *sqlx.DB, templates *ChecklistTemplateService, audit *SystemAuditLogService, users *UserService) *ChecklistShareService {
	return &ChecklistShareService{db: db, templates: templates, audit: audit, users: users}
}

// ShareGrantInput is the POST body for granting a share. Exactly one
// of the recipient_* fields must be set, matching recipient_kind.
type ShareGrantInput struct {
	RecipientKind string     `json:"recipient_kind"`
	UserID        *uuid.UUID `json:"recipient_user_id,omitempty"`
	Office        string     `json:"recipient_office,omitempty"`
	PartnerUnitID *uuid.UUID `json:"recipient_partner_unit_id,omitempty"`
	ProjectID     *uuid.UUID `json:"recipient_project_id,omitempty"`
}

// Share is the row shape returned from list / grant calls.
type Share struct {
	ID                     uuid.UUID  `db:"id"                          json:"id"`
	ChecklistID            uuid.UUID  `db:"checklist_id"                json:"checklist_id"`
	RecipientKind          string     `db:"recipient_kind"              json:"recipient_kind"`
	RecipientUserID        *uuid.UUID `db:"recipient_user_id"           json:"recipient_user_id,omitempty"`
	RecipientOffice        *string    `db:"recipient_office"            json:"recipient_office,omitempty"`
	RecipientPartnerUnitID *uuid.UUID `db:"recipient_partner_unit_id"   json:"recipient_partner_unit_id,omitempty"`
	RecipientProjectID     *uuid.UUID `db:"recipient_project_id"        json:"recipient_project_id,omitempty"`
	GrantedBy              uuid.UUID  `db:"granted_by"                  json:"granted_by"`
	GrantedAt              time.Time  `db:"granted_at"                  json:"granted_at"`
	// Display-name enrichment for the recipient — owners want to see
	// "Sarah Schmidt" not just a UUID on the grants list.
	RecipientLabel string `db:"recipient_label" json:"recipient_label"`
}

// Grant creates a new share row. Caller must own the parent checklist
// (or be global_admin). Recipient validity (FK targets exist + kind
// matches the populated recipient_* column) enforced before INSERT.
func (s *ChecklistShareService) Grant(ctx context.Context, callerID uuid.UUID, slug string, input ShareGrantInput) (*Share, error) {
	row, err := s.templates.GetBySlug(ctx, callerID, slug)
	if err != nil {
		return nil, err
	}
	// Ownership check — Grant is owner-only (global_admin can demote
	// global templates but doesn't author shares).
	if row.OwnerID != callerID {
		return nil, fmt.Errorf("%w: only the owner can grant shares", ErrForbidden)
	}

	kind := strings.ToLower(strings.TrimSpace(input.RecipientKind))
	if err := validateShareInput(kind, input); 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 grant tx: %w", err)
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx,
		`INSERT INTO paliad.checklist_shares
		   (id, checklist_id, recipient_kind, recipient_user_id, recipient_office,
		    recipient_partner_unit_id, recipient_project_id, granted_by, granted_at)
		 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
		id, row.ID, kind,
		input.UserID, nullableString(input.Office), input.PartnerUnitID, input.ProjectID,
		callerID, now,
	); err != nil {
		// Map the partial-unique-index conflict into a friendly 409.
		if pqUniqueViolation(err) {
			return nil, fmt.Errorf("%w: this recipient already has a grant on this checklist", ErrInvalidInput)
		}
		return nil, fmt.Errorf("insert checklist_share: %w", err)
	}

	actorEmail, _ := s.actorEmail(ctx, callerID)
	meta := map[string]any{
		"checklist_id":   row.ID,
		"slug":           slug,
		"share_id":       id,
		"recipient_kind": kind,
	}
	switch kind {
	case "user":
		meta["recipient_user_id"] = input.UserID
	case "office":
		meta["recipient_office"] = input.Office
	case "partner_unit":
		meta["recipient_partner_unit_id"] = input.PartnerUnitID
	case "project":
		meta["recipient_project_id"] = input.ProjectID
	}
	if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
		EventType:  "checklist.shared",
		ActorID:    callerID,
		ActorEmail: actorEmail,
		Metadata:   meta,
	}); err != nil {
		return nil, err
	}

	if err := tx.Commit(); err != nil {
		return nil, fmt.Errorf("commit grant: %w", err)
	}
	return s.getShareByID(ctx, callerID, id)
}

// Revoke deletes a share row. Owner of the parent checklist OR
// global_admin. Audited as 'checklist.unshared' with the recipient meta
// captured pre-delete.
func (s *ChecklistShareService) Revoke(ctx context.Context, callerID uuid.UUID, shareID uuid.UUID) error {
	share, err := s.getShareByID(ctx, callerID, shareID)
	if err != nil {
		return err
	}
	// Resolve owner of the parent checklist for the authorization gate.
	// templates.GetBySlug needs a slug we don't have; inline a minimal
	// owner lookup keyed on the share's checklist_id.
	var ownerID uuid.UUID
	if err := s.db.GetContext(ctx, &ownerID,
		`SELECT owner_id FROM paliad.checklists WHERE id = $1`, share.ChecklistID); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return ErrNotVisible
		}
		return fmt.Errorf("fetch checklist owner: %w", err)
	}
	if ownerID != callerID {
		user, err := s.users.GetByID(ctx, callerID)
		if err != nil {
			return err
		}
		if user == nil || user.GlobalRole != "global_admin" {
			return fmt.Errorf("%w: only the owner or a global_admin can revoke a share", ErrForbidden)
		}
	}

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

	if _, err := tx.ExecContext(ctx,
		`DELETE FROM paliad.checklist_shares WHERE id = $1`, shareID); err != nil {
		return fmt.Errorf("delete checklist_share: %w", err)
	}

	actorEmail, _ := s.actorEmail(ctx, callerID)
	meta := map[string]any{
		"checklist_id":   share.ChecklistID,
		"share_id":       share.ID,
		"recipient_kind": share.RecipientKind,
	}
	switch share.RecipientKind {
	case "user":
		meta["recipient_user_id"] = share.RecipientUserID
	case "office":
		meta["recipient_office"] = share.RecipientOffice
	case "partner_unit":
		meta["recipient_partner_unit_id"] = share.RecipientPartnerUnitID
	case "project":
		meta["recipient_project_id"] = share.RecipientProjectID
	}
	if err := s.audit.WriteChecklistEvent(ctx, tx, ChecklistAuditEvent{
		EventType:  "checklist.unshared",
		ActorID:    callerID,
		ActorEmail: actorEmail,
		Metadata:   meta,
	}); err != nil {
		return err
	}
	return tx.Commit()
}

// ListGrants returns every share row for the checklist. Owner-only —
// recipients only learn about shares affecting them implicitly via the
// visibility predicate.
func (s *ChecklistShareService) ListGrants(ctx context.Context, callerID uuid.UUID, slug string) ([]Share, error) {
	row, err := s.templates.GetBySlug(ctx, callerID, slug)
	if err != nil {
		return nil, err
	}
	if row.OwnerID != callerID {
		user, err := s.users.GetByID(ctx, callerID)
		if err != nil {
			return nil, err
		}
		if user == nil || user.GlobalRole != "global_admin" {
			return nil, fmt.Errorf("%w: only the owner or a global_admin can list shares", ErrForbidden)
		}
	}

	rows := []Share{}
	q := `SELECT s.id, s.checklist_id, s.recipient_kind, s.recipient_user_id,
	             s.recipient_office, s.recipient_partner_unit_id, s.recipient_project_id,
	             s.granted_by, s.granted_at,
	             COALESCE(
	                 CASE s.recipient_kind
	                     WHEN 'user'         THEN ru.display_name
	                     WHEN 'office'       THEN s.recipient_office
	                     WHEN 'partner_unit' THEN pu.name
	                     WHEN 'project'      THEN COALESCE(pr.reference, pr.title)
	                 END,
	                 ''
	             ) AS recipient_label
	        FROM paliad.checklist_shares s
	        LEFT JOIN paliad.users         ru ON ru.id = s.recipient_user_id
	        LEFT JOIN paliad.partner_units pu ON pu.id = s.recipient_partner_unit_id
	        LEFT JOIN paliad.projects      pr ON pr.id = s.recipient_project_id
	       WHERE s.checklist_id = $1
	       ORDER BY s.granted_at DESC`
	if err := s.db.SelectContext(ctx, &rows, q, row.ID); err != nil {
		return nil, fmt.Errorf("list checklist_shares: %w", err)
	}
	return rows, nil
}

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

func (s *ChecklistShareService) getShareByID(ctx context.Context, callerID uuid.UUID, shareID uuid.UUID) (*Share, error) {
	var row Share
	q := `SELECT s.id, s.checklist_id, s.recipient_kind, s.recipient_user_id,
	             s.recipient_office, s.recipient_partner_unit_id, s.recipient_project_id,
	             s.granted_by, s.granted_at,
	             COALESCE(
	                 CASE s.recipient_kind
	                     WHEN 'user'         THEN ru.display_name
	                     WHEN 'office'       THEN s.recipient_office
	                     WHEN 'partner_unit' THEN pu.name
	                     WHEN 'project'      THEN COALESCE(pr.reference, pr.title)
	                 END,
	                 ''
	             ) AS recipient_label
	        FROM paliad.checklist_shares s
	        LEFT JOIN paliad.users         ru ON ru.id = s.recipient_user_id
	        LEFT JOIN paliad.partner_units pu ON pu.id = s.recipient_partner_unit_id
	        LEFT JOIN paliad.projects      pr ON pr.id = s.recipient_project_id
	       WHERE s.id = $1`
	err := s.db.GetContext(ctx, &row, q, shareID)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotVisible
	}
	if err != nil {
		return nil, fmt.Errorf("fetch checklist_share: %w", err)
	}
	return &row, nil
}

func (s *ChecklistShareService) actorEmail(ctx context.Context, userID uuid.UUID) (string, error) {
	u, err := s.users.GetByID(ctx, userID)
	if err != nil || u == nil {
		return "", err
	}
	return u.Email, nil
}

// --- pure helpers ---------------------------------------------------------

func validateShareInput(kind string, input ShareGrantInput) error {
	switch kind {
	case "user":
		if input.UserID == nil {
			return fmt.Errorf("%w: recipient_user_id required when recipient_kind=user", ErrInvalidInput)
		}
	case "office":
		off := strings.TrimSpace(input.Office)
		if off == "" {
			return fmt.Errorf("%w: recipient_office required when recipient_kind=office", ErrInvalidInput)
		}
		if !offices.IsValid(off) {
			return fmt.Errorf("%w: unknown office %q", ErrInvalidInput, off)
		}
	case "partner_unit":
		if input.PartnerUnitID == nil {
			return fmt.Errorf("%w: recipient_partner_unit_id required when recipient_kind=partner_unit", ErrInvalidInput)
		}
	case "project":
		if input.ProjectID == nil {
			return fmt.Errorf("%w: recipient_project_id required when recipient_kind=project", ErrInvalidInput)
		}
	default:
		return fmt.Errorf("%w: recipient_kind must be user|office|partner_unit|project, got %q", ErrInvalidInput, kind)
	}
	return nil
}

func nullableString(s string) any {
	t := strings.TrimSpace(s)
	if t == "" {
		return nil
	}
	return t
}

// pqUniqueViolation reports whether the error is a Postgres
// unique_violation (SQLSTATE 23505). lib/pq exposes it via the .Code()
// method; sqlx surfaces it untouched. We sniff via the error string to
// avoid pulling in lib/pq's Error type here.
func pqUniqueViolation(err error) bool {
	if err == nil {
		return false
	}
	msg := err.Error()
	return strings.Contains(msg, "23505") || strings.Contains(msg, "duplicate key")
}
