package mail

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"path/filepath"
	"strings"

	"github.com/google/uuid"
	"github.com/jackc/pgx/v5"

	"github.com/ultisuite/ulti-backend/internal/mail/limits"
	"github.com/ultisuite/ulti-backend/internal/mail/storage"
)

var (
	ErrAttachmentNotFound       = errors.New("attachment not found")
	ErrAttachmentTooLarge       = limits.ErrAttachmentTooLarge
	ErrTooManyAttachments       = limits.ErrTooManyAttachments
	ErrAttachmentsTotalTooLarge = limits.ErrAttachmentsTotalTooLarge
)

type draftAttachmentRef struct {
	ID          string `json:"id"`
	Filename    string `json:"filename"`
	ContentType string `json:"content_type"`
	Size        int64  `json:"size"`
	S3Bucket    string `json:"s3_bucket"`
	S3Key       string `json:"s3_key"`
	ContentID   string `json:"content_id,omitempty"`
	IsInline    bool   `json:"is_inline"`
}

func (s *Service) ListMessageAttachments(ctx context.Context, externalID, messageID string) ([]map[string]any, error) {
	if _, err := s.ensureMessageOwned(ctx, externalID, messageID); err != nil {
		return nil, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT id, filename, content_type, size, content_id, is_inline, COALESCE(drive_path, ''), virus_scan_status
		FROM attachments WHERE message_id = $1
		ORDER BY created_at ASC
	`, messageID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]map[string]any, 0)
	for rows.Next() {
		var id, filename, contentType, contentID, drivePath, virusScanStatus string
		var size int64
		var isInline bool
		if err := rows.Scan(&id, &filename, &contentType, &size, &contentID, &isInline, &drivePath, &virusScanStatus); err != nil {
			return nil, err
		}
		entry := map[string]any{
			"id": id, "filename": filename, "content_type": contentType,
			"size": size, "is_inline": isInline,
			"virus_scan_status": virusScanStatus,
		}
		if contentID != "" {
			entry["content_id"] = contentID
		}
		if drivePath != "" {
			entry["drive_path"] = drivePath
		}
		out = append(out, entry)
	}
	return out, rows.Err()
}

func (s *Service) MessageAttachmentCIDMap(ctx context.Context, externalID, messageID string) (map[string]string, error) {
	if _, err := s.ensureMessageOwned(ctx, externalID, messageID); err != nil {
		return nil, err
	}

	rows, err := s.db.Query(ctx, `
		SELECT id, content_id, filename, is_inline FROM attachments
		WHERE message_id = $1 AND (content_id <> '' OR is_inline)
	`, messageID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	mapping := make(map[string]string)
	for rows.Next() {
		var id, contentID, filename string
		var isInline bool
		if err := rows.Scan(&id, &contentID, &filename, &isInline); err != nil {
			return nil, err
		}
		if contentID != "" {
			registerCIDMapKeys(mapping, contentID, id)
		}
		if isInline && filename != "" {
			registerCIDMapKeys(mapping, filepath.Base(filename), id)
		}
	}
	return mapping, rows.Err()
}

func registerCIDMapKeys(mapping map[string]string, contentID, attachmentID string) {
	key := strings.Trim(contentID, "<> \t")
	if key == "" {
		return
	}
	mapping[key] = attachmentID
	mapping[strings.ToLower(key)] = attachmentID
	mapping["cid:"+key] = attachmentID
	mapping["cid:"+strings.ToLower(key)] = attachmentID
}

func (s *Service) UploadMessageAttachment(
	ctx context.Context, externalID, messageID, filename, contentType, contentID string,
	isInline bool, reader io.Reader, size int64,
) (string, error) {
	if s.storage == nil {
		return "", errors.New("object storage unavailable")
	}
	if err := limits.ValidateAttachmentSize(size); err != nil {
		return "", err
	}
	userID, err := s.ensureMessageOwned(ctx, externalID, messageID)
	if err != nil {
		return "", err
	}

	var count int
	var totalSize int64
	err = s.db.QueryRow(ctx, `
		SELECT COUNT(*)::int, COALESCE(SUM(size), 0)::bigint
		FROM attachments WHERE message_id = $1
	`, messageID).Scan(&count, &totalSize)
	if err != nil {
		return "", err
	}
	if err := limits.ValidateAttachmentQuota(count, totalSize, size); err != nil {
		return "", err
	}

	objectKey := storage.MessageObjectKey(userID, messageID, filename)
	scanStatus := "skipped"
	putReader := reader
	putSize := size
	if s.scanner != nil {
		data, result, err := s.scanner.ScanReader(ctx, filename, reader, size)
		if err != nil {
			return "", err
		}
		scanStatus = result.Status
		putReader = bytes.NewReader(data)
		putSize = int64(len(data))
	}
	if err := s.storage.Put(ctx, objectKey, putReader, putSize, contentType); err != nil {
		return "", err
	}

	var id string
	err = s.db.QueryRow(ctx, `
		INSERT INTO attachments (message_id, filename, content_type, size, s3_bucket, s3_key, content_id, is_inline, virus_scan_status)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		RETURNING id
	`, messageID, filename, contentType, putSize, s.storageBucket(), objectKey, contentID, isInline, scanStatus).Scan(&id)
	if err != nil {
		_ = s.storage.Delete(ctx, objectKey)
		return "", err
	}

	_, err = s.db.Exec(ctx, `
		UPDATE messages SET has_attachments = true, updated_at = NOW() WHERE id = $1
	`, messageID)
	if err != nil {
		return "", err
	}
	return id, nil
}

func (s *Service) OpenAttachment(ctx context.Context, externalID, attachmentID string) (
	filename, contentType string, size int64, isInline bool, body io.ReadCloser, err error,
) {
	if s.storage == nil {
		return "", "", 0, false, nil, errors.New("object storage unavailable")
	}

	var s3Key string
	err = s.db.QueryRow(ctx, `
		SELECT a.filename, a.content_type, a.size, a.is_inline, a.s3_key
		FROM attachments a
		JOIN messages m ON a.message_id = m.id
		JOIN mail_accounts ma ON m.account_id = ma.id
		WHERE a.id = $1 AND ma.user_id = (SELECT id FROM users WHERE external_id = $2)
	`, attachmentID, externalID).Scan(&filename, &contentType, &size, &isInline, &s3Key)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", "", 0, false, nil, ErrAttachmentNotFound
		}
		return "", "", 0, false, nil, err
	}

	obj, err := s.storage.Get(ctx, s3Key)
	if err != nil {
		return "", "", 0, false, nil, err
	}
	return filename, contentType, size, isInline, obj, nil
}

func (s *Service) UploadDraftAttachment(
	ctx context.Context, externalID, draftID, filename, contentType, contentID string,
	isInline bool, reader io.Reader, size int64,
) (string, error) {
	if s.storage == nil {
		return "", errors.New("object storage unavailable")
	}
	if err := limits.ValidateAttachmentSize(size); err != nil {
		return "", err
	}

	userID, err := s.ResolveUserID(ctx, externalID)
	if err != nil {
		return "", err
	}

	var attachmentsJSON []byte
	err = s.db.QueryRow(ctx, `
		SELECT attachments FROM outbox
		WHERE id = $1 AND user_id = $2 AND status = 'draft'
	`, draftID, userID).Scan(&attachmentsJSON)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrNotFound
		}
		return "", err
	}

	objectKey := storage.DraftObjectKey(userID, draftID, filename)
	putReader := reader
	putSize := size
	if s.scanner != nil {
		data, _, err := s.scanner.ScanReader(ctx, filename, reader, size)
		if err != nil {
			return "", err
		}
		putReader = bytes.NewReader(data)
		putSize = int64(len(data))
	}
	if err := s.storage.Put(ctx, objectKey, putReader, putSize, contentType); err != nil {
		return "", err
	}

	refs := make([]draftAttachmentRef, 0)
	if len(attachmentsJSON) > 0 && string(attachmentsJSON) != "[]" {
		_ = json.Unmarshal(attachmentsJSON, &refs)
	}

	var totalSize int64
	for _, ref := range refs {
		totalSize += ref.Size
	}
	if err := limits.ValidateAttachmentQuota(len(refs), totalSize, putSize); err != nil {
		return "", err
	}

	attID := uuid.NewString()
	refs = append(refs, draftAttachmentRef{
		ID: attID, Filename: filename, ContentType: contentType, Size: putSize,
		S3Bucket: s.storageBucket(), S3Key: objectKey,
		ContentID: contentID, IsInline: isInline,
	})

	updated, _ := json.Marshal(refs)
	result, err := s.db.Exec(ctx, `
		UPDATE outbox SET attachments = $1, updated_at = NOW()
		WHERE id = $2 AND user_id = $3 AND status = 'draft'
	`, updated, draftID, userID)
	if err != nil {
		_ = s.storage.Delete(ctx, objectKey)
		return "", err
	}
	if result.RowsAffected() == 0 {
		_ = s.storage.Delete(ctx, objectKey)
		return "", ErrNotFound
	}
	return attID, nil
}

func (s *Service) OpenDraftAttachment(ctx context.Context, externalID, draftID, attachmentID string) (
	filename, contentType string, body io.ReadCloser, err error,
) {
	if s.storage == nil {
		return "", "", nil, errors.New("object storage unavailable")
	}

	userID, err := s.ResolveUserID(ctx, externalID)
	if err != nil {
		return "", "", nil, err
	}

	var attachmentsJSON []byte
	err = s.db.QueryRow(ctx, `
		SELECT attachments FROM outbox
		WHERE id = $1 AND user_id = $2 AND status = 'draft'
	`, draftID, userID).Scan(&attachmentsJSON)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", "", nil, ErrNotFound
		}
		return "", "", nil, err
	}

	var refs []draftAttachmentRef
	if err := json.Unmarshal(attachmentsJSON, &refs); err != nil {
		return "", "", nil, err
	}
	for _, ref := range refs {
		if ref.ID != attachmentID {
			continue
		}
		obj, err := s.storage.Get(ctx, ref.S3Key)
		if err != nil {
			return "", "", nil, err
		}
		return ref.Filename, ref.ContentType, obj, nil
	}
	return "", "", nil, ErrAttachmentNotFound
}

func (s *Service) ensureMessageOwned(ctx context.Context, externalID, messageID string) (userID string, err error) {
	err = s.db.QueryRow(ctx, `
		SELECT u.id FROM messages m
		JOIN mail_accounts ma ON m.account_id = ma.id
		JOIN users u ON ma.user_id = u.id
		WHERE m.id = $1 AND u.external_id = $2
	`, messageID, externalID).Scan(&userID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrNotFound
		}
		return "", err
	}
	return userID, nil
}

func (s *Service) storageBucket() string {
	if s.attachmentsBucket != "" {
		return s.attachmentsBucket
	}
	return "mail-attachments"
}
