package app

import (
	"archive/zip"
	"bufio"
	"bytes"
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

const maxMailImportBytes int64 = 256 << 20

var exportFilenameUnsafe = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)

func (a *App) handleExportMail(w http.ResponseWriter, r *http.Request) {
	ids, err := a.exportMessageIDs(r)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			respondError(w, http.StatusNotFound, "mailbox or label not found")
			return
		}
		if errors.Is(err, errSystemAdminRequired) {
			respondError(w, http.StatusForbidden, "system admin required")
			return
		}
		badRequest(w, err)
		return
	}

	filename := fmt.Sprintf("mail-export-%s.zip", a.now().UTC().Format("20060102-150405"))
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
	w.Header().Set("Cache-Control", "no-store")

	zw := zip.NewWriter(w)
	usedNames := make(map[string]int, len(ids))
	for index, id := range ids {
		raw, subject, err := a.rawMessageForExport(r.Context(), id)
		if err != nil {
			_ = zw.Close()
			return
		}
		entryName := uniqueExportFilename(exportMessageFilename(subject, id, index), usedNames)
		entry, err := zw.CreateHeader(&zip.FileHeader{Name: entryName, Method: zip.Deflate})
		if err != nil {
			_ = zw.Close()
			return
		}
		if _, err := entry.Write(raw); err != nil {
			_ = zw.Close()
			return
		}
	}
	_ = zw.Close()
}

var errSystemAdminRequired = errors.New("system admin required")

func (a *App) exportMessageIDs(r *http.Request) ([]string, error) {
	user := currentUser(r)
	if user == nil {
		return nil, errors.New("no user")
	}
	view := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("view")))
	mailboxID := strings.TrimSpace(r.URL.Query().Get("mailboxId"))
	where := []string{}
	args := []any{}

	if view == "unknown" {
		if user.Role != "admin" {
			return nil, errSystemAdminRequired
		}
		where = append(where, "m.mailbox_id IS NULL")
	} else {
		where = append(where, "EXISTS (SELECT 1 FROM mailboxes owner_mb WHERE owner_mb.id=m.mailbox_id AND owner_mb.user_id=? AND owner_mb.status='active')")
		args = append(args, user.ID)
		if mailboxID != "" && !isAllMailboxID(mailboxID) {
			if _, err := a.mailboxForCurrentUserWithID(r, mailboxID); err != nil {
				return nil, err
			}
			where = append(where, "m.mailbox_id=?")
			args = append(args, mailboxID)
		}
		switch view {
		case "", "folder":
			folder := strings.TrimSpace(r.URL.Query().Get("folder"))
			if folder == "" {
				folder = "Inbox"
			}
			normalized, err := normalizeFolderNameForUser(folder)
			if err != nil {
				return nil, err
			}
			where = append(where, "f.name=?")
			args = append(args, normalized)
		case "starred":
			where = append(where, "m.is_starred=1")
		case "label":
			labelID := strings.TrimSpace(r.URL.Query().Get("labelId"))
			if labelID == "" || !a.labelBelongsToUser(r.Context(), labelID, user.ID) {
				return nil, sql.ErrNoRows
			}
			where = append(where, "EXISTS (SELECT 1 FROM message_labels ml WHERE ml.message_id=m.id AND ml.label_id=?)")
			args = append(args, labelID)
		default:
			return nil, errors.New("unsupported mail view")
		}
	}

	rows, err := a.db.QueryContext(r.Context(), `SELECT m.id FROM messages m LEFT JOIN folders f ON f.id=m.folder_id WHERE `+strings.Join(where, " AND ")+` ORDER BY m.received_at DESC,m.id`, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	ids := []string{}
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	return ids, rows.Err()
}

func (a *App) rawMessageForExport(ctx context.Context, id string) ([]byte, string, error) {
	msg, err := a.storedMessageByID(ctx, id)
	if err != nil {
		return nil, "", err
	}
	if msg.RawPath != "" {
		if ok, pathErr := a.pathIsUnderMaildirRoot(msg.RawPath); pathErr == nil && ok {
			if raw, readErr := os.ReadFile(msg.RawPath); readErr == nil {
				return raw, msg.Subject, nil
			}
		}
	}
	attachments, err := a.attachmentInputsForMessage(ctx, id)
	if err != nil {
		return nil, "", err
	}
	raw, err := BuildMIME(MIMEMessage{
		From: msg.From, FromName: msg.FromName, To: msg.To, CC: msg.CC, BCC: msg.BCC,
		Subject: msg.Subject, Text: msg.BodyText, HTML: msg.BodyHTML, MessageID: msg.MessageID,
		Date: messageDate(msg), Attachments: attachments,
	})
	return raw, msg.Subject, err
}

func exportMessageFilename(subject, id string, index int) string {
	name := exportFilenameUnsafe.ReplaceAllString(strings.TrimSpace(subject), "-")
	name = strings.Trim(name, ".-_")
	if name == "" {
		name = "message"
	}
	if len(name) > 80 {
		name = name[:80]
	}
	return fmt.Sprintf("%04d-%s-%s.eml", index+1, name, id)
}

func uniqueExportFilename(name string, used map[string]int) string {
	used[name]++
	if used[name] == 1 {
		return name
	}
	base := strings.TrimSuffix(name, filepath.Ext(name))
	return fmt.Sprintf("%s-%d%s", base, used[name], filepath.Ext(name))
}

func (a *App) handleImportMail(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, maxMailImportBytes)
	if err := r.ParseMultipartForm(maxMailImportBytes); err != nil {
		respondError(w, http.StatusRequestEntityTooLarge, "import is too large")
		return
	}
	if r.MultipartForm != nil {
		defer r.MultipartForm.RemoveAll()
	}
	mb, err := a.mailboxForCurrentUserWithID(r, r.FormValue("mailboxId"))
	if err != nil {
		respondError(w, http.StatusNotFound, "mailbox not found")
		return
	}
	folderName := strings.TrimSpace(r.FormValue("folder"))
	if folderName == "" {
		folderName = "Inbox"
	}
	folderName, err = normalizeFolderNameForUser(folderName)
	if err != nil {
		badRequest(w, err)
		return
	}
	folderID, err := a.ensureFolder(r.Context(), mb.ID, folderName)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "failed to load folder")
		return
	}
	files := r.MultipartForm.File["files"]
	if len(files) == 0 {
		files = r.MultipartForm.File["file"]
	}
	if len(files) == 0 {
		badRequest(w, errors.New("at least one EML or MBOX file is required"))
		return
	}

	imported, skipped := 0, 0
	problems := []string{}
	maxMessageBytes := int64(a.cfg.SubmissionMaxMessageMB) * 1024 * 1024
	if maxMessageBytes <= 0 {
		maxMessageBytes = 35 * 1024 * 1024
	}
	for _, header := range files {
		messages, fileErr := readImportFile(header, maxMessageBytes)
		if fileErr != nil {
			skipped++
			problems = appendImportProblem(problems, fmt.Sprintf("%s: %v", header.Filename, fileErr))
			continue
		}
		for _, raw := range messages {
			if err := a.importRawMessage(r.Context(), mb, folderID, raw); err != nil {
				skipped++
				problems = appendImportProblem(problems, fmt.Sprintf("%s: %v", header.Filename, err))
				continue
			}
			imported++
		}
	}
	if imported == 0 && len(problems) > 0 {
		badRequest(w, errors.New(problems[0]))
		return
	}
	respondJSON(w, http.StatusOK, map[string]any{"ok": true, "imported": imported, "skipped": skipped, "errors": problems})
}

func readImportFile(header *multipart.FileHeader, maxMessageBytes int64) ([][]byte, error) {
	ext := strings.ToLower(filepath.Ext(header.Filename))
	if ext != ".eml" && ext != ".mbox" {
		return nil, errors.New("only .eml and .mbox files are supported")
	}
	file, err := header.Open()
	if err != nil {
		return nil, err
	}
	defer file.Close()
	if ext == ".eml" {
		raw, err := io.ReadAll(io.LimitReader(file, maxMessageBytes+1))
		if err != nil {
			return nil, err
		}
		if int64(len(raw)) > maxMessageBytes {
			return nil, fmt.Errorf("message exceeds %d MB", maxMessageBytes/(1024*1024))
		}
		if len(bytes.TrimSpace(raw)) == 0 {
			return nil, errors.New("message is empty")
		}
		return [][]byte{raw}, nil
	}
	return parseMBOX(file, maxMessageBytes)
}

func parseMBOX(reader io.Reader, maxMessageBytes int64) ([][]byte, error) {
	scanner := bufio.NewScanner(reader)
	bufferSize := int(maxMessageBytes + 1024)
	if bufferSize < 64*1024 {
		bufferSize = 64 * 1024
	}
	scanner.Buffer(make([]byte, 64*1024), bufferSize)
	var current bytes.Buffer
	messages := [][]byte{}
	seenSeparator := false
	flush := func() error {
		raw := bytes.TrimSpace(current.Bytes())
		current.Reset()
		if len(raw) == 0 {
			return nil
		}
		if int64(len(raw)) > maxMessageBytes {
			return fmt.Errorf("message exceeds %d MB", maxMessageBytes/(1024*1024))
		}
		messages = append(messages, append([]byte(nil), raw...))
		return nil
	}
	for scanner.Scan() {
		line := scanner.Bytes()
		if bytes.HasPrefix(line, []byte("From ")) {
			if seenSeparator {
				if err := flush(); err != nil {
					return nil, err
				}
			}
			seenSeparator = true
			continue
		}
		if bytes.HasPrefix(line, []byte(">From ")) {
			line = line[1:]
		}
		current.Write(line)
		current.WriteString("\r\n")
		if int64(current.Len()) > maxMessageBytes {
			return nil, fmt.Errorf("message exceeds %d MB", maxMessageBytes/(1024*1024))
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	if err := flush(); err != nil {
		return nil, err
	}
	if len(messages) == 0 {
		return nil, errors.New("MBOX contains no messages")
	}
	return messages, nil
}

func (a *App) importRawMessage(ctx context.Context, mb *Mailbox, folderID string, raw []byte) error {
	msg, attachments, err := a.parseMaildirMessage(raw, mb.Address)
	if err != nil {
		return fmt.Errorf("invalid message: %w", err)
	}
	msg.MailboxID = mb.ID
	msg.FolderID = folderID
	msg.RecipientAddr = mb.Address
	msg.RawPath = ""
	id, err := a.insertMessage(ctx, msg, attachments)
	if err != nil {
		return err
	}
	if err := a.writeRawMessageToMaildir(ctx, id, raw, false); err != nil {
		a.deleteMessage(ctx, id)
		return err
	}
	return nil
}

func appendImportProblem(items []string, problem string) []string {
	if len(items) >= 5 {
		return items
	}
	return append(items, problem)
}
