package mail

import (
	"context"
	"encoding/json"
	"errors"

	"github.com/jackc/pgx/v5"

	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/mail/sanitize"
	"github.com/ultisuite/ulti-backend/internal/mail/threading"
)

type DraftsList struct {
	Drafts     []map[string]any     `json:"drafts"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListDrafts(ctx context.Context, externalID string, params query.ListParams) (DraftsList, error) {
	var total int64
	err := s.db.QueryRow(ctx, `
		SELECT COUNT(*) FROM outbox o
		WHERE o.user_id = (SELECT id FROM users WHERE external_id = $1)
		  AND o.status = 'draft'
	`, externalID).Scan(&total)
	if err != nil {
		return DraftsList{}, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT o.id, o.account_id, o.identity_id, o.to_addrs, o.cc_addrs, o.bcc_addrs,
		       o.subject, o.body_text, o.updated_at, o.created_at
		FROM outbox o
		WHERE o.user_id = (SELECT id FROM users WHERE external_id = $1)
		  AND o.status = 'draft'
		ORDER BY o.updated_at DESC
		LIMIT $2 OFFSET $3
	`, externalID, params.Limit(), params.Offset())
	if err != nil {
		return DraftsList{}, err
	}
	defer rows.Close()

	drafts := make([]map[string]any, 0)
	for rows.Next() {
		entry, err := scanDraftListRow(rows)
		if err != nil {
			return DraftsList{}, err
		}
		drafts = append(drafts, entry)
	}
	if err := rows.Err(); err != nil {
		return DraftsList{}, err
	}

	return DraftsList{
		Drafts:     drafts,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) GetDraft(ctx context.Context, externalID, draftID string) (map[string]any, error) {
	var (
		id, accountID, subject, bodyText, bodyHTML, inReplyTo string
		identityID                                            *string
		toAddrs, ccAddrs, bccAddrs, attachments               []byte
		references                                            []string
		createdAt, updatedAt                                  any
	)
	err := s.db.QueryRow(ctx, `
		SELECT o.id, o.account_id, o.identity_id, o.to_addrs, o.cc_addrs, o.bcc_addrs,
		       o.subject, o.body_text, o.body_html, o.in_reply_to, o.references_header,
		       o.attachments, o.created_at, o.updated_at
		FROM outbox o
		WHERE o.id = $1
		  AND o.user_id = (SELECT id FROM users WHERE external_id = $2)
		  AND o.status = 'draft'
	`, draftID, externalID).Scan(
		&id, &accountID, &identityID, &toAddrs, &ccAddrs, &bccAddrs,
		&subject, &bodyText, &bodyHTML, &inReplyTo, &references,
		&attachments, &createdAt, &updatedAt,
	)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	return draftDetailMap(id, accountID, identityID, toAddrs, ccAddrs, bccAddrs, subject, bodyText, bodyHTML, inReplyTo, references, attachments, createdAt, updatedAt), nil
}

func (s *Service) CreateDraft(ctx context.Context, userID string, req *draftRequest) (string, error) {
	if err := s.validateDraftAccountAndIdentity(ctx, userID, req.AccountID, req.IdentityID); err != nil {
		return "", err
	}

	toJSON, _ := json.Marshal(req.To)
	ccJSON, _ := json.Marshal(req.Cc)
	bccJSON, _ := json.Marshal(req.Bcc)
	attachmentsJSON, _ := json.Marshal(req.Attachments)
	if req.Attachments == nil {
		attachmentsJSON = []byte("[]")
	}
	inReplyTo := threading.NormalizeMessageID(req.InReplyTo)

	var id string
	err := s.db.QueryRow(ctx, `
		INSERT INTO outbox (
			user_id, account_id, identity_id, to_addrs, cc_addrs, bcc_addrs,
			subject, body_text, body_html, in_reply_to, references_header, attachments, status
		)
		SELECT $1, ma.id, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'draft'
		FROM mail_accounts ma
		WHERE ma.id = $2 AND ma.user_id = $1
		RETURNING id
	`, userID, req.AccountID, nilIfEmpty(req.IdentityID), toJSON, ccJSON, bccJSON,
		req.Subject, req.BodyText, req.BodyHTML, inReplyTo, []string{}, attachmentsJSON).Scan(&id)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrAccountNotFound
		}
		return "", err
	}
	return id, nil
}

func (s *Service) UpdateDraft(ctx context.Context, externalID, draftID string, req *draftRequest) error {
	userID, err := s.ResolveUserID(ctx, externalID)
	if err != nil {
		return err
	}

	accountID := req.AccountID
	if accountID == "" && req.IdentityID != "" {
		err := s.db.QueryRow(ctx, `
			SELECT account_id FROM outbox
			WHERE id = $1 AND user_id = $2 AND status = 'draft'
		`, draftID, userID).Scan(&accountID)
		if err != nil {
			if errors.Is(err, pgx.ErrNoRows) {
				return ErrNotFound
			}
			return err
		}
	}
	if accountID != "" {
		if err := s.validateDraftAccountAndIdentity(ctx, userID, accountID, req.IdentityID); err != nil {
			return err
		}
	}

	toJSON, _ := json.Marshal(req.To)
	ccJSON, _ := json.Marshal(req.Cc)
	bccJSON, _ := json.Marshal(req.Bcc)
	attachmentsJSON, _ := json.Marshal(req.Attachments)
	if req.Attachments == nil {
		attachmentsJSON = []byte("[]")
	}
	inReplyTo := threading.NormalizeMessageID(req.InReplyTo)

	result, err := s.db.Exec(ctx, `
		UPDATE outbox o SET
			account_id = COALESCE($1, o.account_id),
			identity_id = CASE WHEN $2 <> '' THEN $2::uuid ELSE o.identity_id END,
			to_addrs = $3,
			cc_addrs = $4,
			bcc_addrs = $5,
			subject = $6,
			body_text = $7,
			body_html = $8,
			in_reply_to = $9,
			references_header = $10,
			attachments = $11,
			updated_at = NOW()
		WHERE o.id = $12
		  AND o.user_id = $13
		  AND o.status = 'draft'
	`, nilIfEmpty(req.AccountID), req.IdentityID, toJSON, ccJSON, bccJSON,
		req.Subject, req.BodyText, req.BodyHTML, inReplyTo, []string{}, attachmentsJSON,
		draftID, userID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (s *Service) DeleteDraft(ctx context.Context, externalID, draftID string) error {
	result, err := s.db.Exec(ctx, `
		DELETE FROM outbox o
		WHERE o.id = $1
		  AND o.user_id = (SELECT id FROM users WHERE external_id = $2)
		  AND o.status = 'draft'
	`, draftID, externalID)
	if err != nil {
		return err
	}
	if result.RowsAffected() == 0 {
		return ErrNotFound
	}
	return nil
}

func (s *Service) validateDraftAccountAndIdentity(ctx context.Context, userID, accountID, identityID string) error {
	var exists bool
	err := s.db.QueryRow(ctx, `
		SELECT EXISTS(SELECT 1 FROM mail_accounts WHERE id = $1 AND user_id = $2)
	`, accountID, userID).Scan(&exists)
	if err != nil {
		return err
	}
	if !exists {
		return ErrAccountNotFound
	}
	if identityID == "" {
		return nil
	}
	err = s.db.QueryRow(ctx, `
		SELECT EXISTS(SELECT 1 FROM mail_identities WHERE id = $1 AND account_id = $2)
	`, identityID, accountID).Scan(&exists)
	if err != nil {
		return err
	}
	if !exists {
		return ErrNotFound
	}
	return nil
}

type draftListScanner interface {
	Scan(dest ...any) error
}

func scanDraftListRow(rows draftListScanner) (map[string]any, error) {
	var id, accountID, subject, bodyText string
	var identityID *string
	var toAddrs, ccAddrs, bccAddrs []byte
	var updatedAt, createdAt any
	if err := rows.Scan(&id, &accountID, &identityID, &toAddrs, &ccAddrs, &bccAddrs, &subject, &bodyText, &updatedAt, &createdAt); err != nil {
		return nil, err
	}
	entry := map[string]any{
		"id": id, "account_id": accountID, "subject": subject,
		"to": json.RawMessage(toAddrs), "cc": json.RawMessage(ccAddrs), "bcc": json.RawMessage(bccAddrs),
		"body_text": bodyText, "updated_at": updatedAt, "created_at": createdAt,
	}
	if identityID != nil {
		entry["identity_id"] = *identityID
	}
	return entry, nil
}

func draftDetailMap(
	id, accountID string,
	identityID *string,
	toAddrs, ccAddrs, bccAddrs []byte,
	subject, bodyText, bodyHTML, inReplyTo string,
	references []string,
	attachments []byte,
	createdAt, updatedAt any,
) map[string]any {
	out := map[string]any{
		"id": id, "account_id": accountID, "subject": subject,
		"to": json.RawMessage(toAddrs), "cc": json.RawMessage(ccAddrs), "bcc": json.RawMessage(bccAddrs),
		"body_text": bodyText, "body_html": sanitize.SanitizeHTML(bodyHTML),
		"in_reply_to": inReplyTo, "references": references,
		"attachments": json.RawMessage(attachments),
		"created_at":  createdAt, "updated_at": updatedAt,
	}
	if identityID != nil {
		out["identity_id"] = *identityID
	}
	return out
}
