package webui

import (
	"encoding/base64"
	"html/template"
	"net/http"
	"os"
	"strings"

	"mailgoserver/internal/db"
	"mailgoserver/internal/mailview"
)

// emailLogAccessible checks a scoped admin's domain assignment against the sender
// domain of an email log's MAIL FROM address — these logs predate per-domain admin
// scoping and have no domain_id column, so this is the same text-domain heuristic as
// accessibleDomainNames/emailDomain, not a foreign key.
func (a *App) emailLogAccessible(r *http.Request, mailFrom string) (bool, error) {
	names, isGlobal, err := a.accessibleDomainNames(r)
	if err != nil {
		return false, err
	}
	return isGlobal || names[emailDomain(mailFrom)], nil
}

// viewedAttachment is one attachment ready for the log viewer: decoded bytes encoded
// as a data: URI so no separate download route/disk read is needed, and a browser can
// render it as an inline image directly for the review case this is really for
// (a quarantined message an admin needs to actually inspect, images and all).
type viewedAttachment struct {
	Filename    string
	ContentType string
	Size        int64 // int64 to match humanFileSize's signature (the "filesize" template func)
	DataURI     template.URL
	IsImage     bool
}

// viewMessageContent mirrors view_message.py's view_message_content(). log.MessageBody
// holds the *entire* raw message when this log's content was eligible to be stored
// (see session.go's storeContent) — re-parsed here via mailview (the same parser
// webmail's own message view uses) so the real HTML body, inline images, and
// attachments all render, not just a plain-text approximation. Falls back to showing
// message_body as plain preformatted text if it doesn't parse as a MIME message (e.g.
// an older log row stored before this — plain-text-only — capture existed).
func (a *App) viewMessageContent(w http.ResponseWriter, r *http.Request) {
	log, err := a.DB.GetEmailLogByID(pathID(r))
	if err != nil || log == nil {
		http.NotFound(w, r)
		return
	}
	if ok, err := a.emailLogAccessible(r, log.MailFrom); err != nil || !ok {
		http.NotFound(w, r)
		return
	}

	// The old, file-on-disk attachment mechanism (still opt-in-gated the same way it
	// always was) — kept as a fallback list for log rows predating the raw-message
	// capture below, where this is the only place attachments exist at all.
	legacyAttachments, _ := a.DB.ListAttachmentsForEmail(log.ID)

	var htmlBody template.HTML
	var plainBody string
	var attachments []viewedAttachment
	if log.MessageBody != "" {
		if parsed, err := mailview.Parse([]byte(log.MessageBody)); err == nil {
			if parsed.HTMLBody != "" {
				htmlBody = template.HTML(htmlBodyPolicy.Sanitize(parsed.HTMLBody))
			}
			plainBody = parsed.TextBody
			for _, att := range parsed.Attachments {
				ct := att.ContentType
				if ct == "" {
					ct = "application/octet-stream"
				}
				attachments = append(attachments, viewedAttachment{
					Filename: att.Filename, ContentType: ct, Size: int64(len(att.Data)),
					DataURI: template.URL("data:" + ct + ";base64," + base64.StdEncoding.EncodeToString(att.Data)),
					IsImage: strings.HasPrefix(ct, "image/"),
				})
			}
		} else {
			// Doesn't parse as MIME — treat the stored string as plain text as-is
			// (the shape a pre-fix log row's message_body was always in).
			plainBody = log.MessageBody
		}
	}

	a.render(w, r, "view_message_content.html", M{"active": "logs", "log": M{
		"id": log.ID, "mail_from": log.MailFrom, "to_address": log.ToAddress,
		"cc_addresses": log.CcAddresses, "bcc_addresses": log.BccAddresses,
		"subject": log.Subject, "created_at": log.CreatedAt,
		"html_body": htmlBody, "plain_body": plainBody, "has_content": log.MessageBody != "",
		"email_headers": log.EmailHeaders, "attachments": attachments, "legacy_attachments": legacyAttachments,
	}})
}

var extContentType = map[string]string{
	".txt": "text/plain", ".csv": "text/csv", ".pdf": "application/pdf",
	".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif",
	".svg": "image/svg+xml", ".html": "text/html", ".htm": "text/html",
	".json": "application/json", ".xml": "application/xml", ".md": "text/markdown",
}

// downloadAttachment mirrors view_message.py's download_attachment(), including its
// CSV-to-HTML-table inline preview special case.
func (a *App) downloadAttachment(w http.ResponseWriter, r *http.Request) {
	att, err := a.DB.GetAttachmentByID(pathID(r))
	if err != nil || att == nil {
		http.NotFound(w, r)
		return
	}
	if !a.attachmentAccessible(w, r, att) {
		return
	}
	if _, err := os.Stat(att.FilePath); err != nil {
		setFlash(w, "error", "Attachment file not found on disk")
		http.Redirect(w, r, Prefix+"/logs?type=emails", http.StatusFound)
		return
	}

	contentType := att.ContentType
	if contentType == "" || contentType == "application/octet-stream" {
		if ext := extOfName(att.Filename); ext != "" {
			if ct, ok := extContentType[ext]; ok {
				contentType = ct
			}
		}
	}
	asAttachment := r.URL.Query().Get("download") == "true"

	if contentType == "text/csv" && !asAttachment {
		data, err := os.ReadFile(att.FilePath)
		if err != nil {
			http.Error(w, "read error", http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Write([]byte("<table border=1>"))
		for _, line := range strings.Split(string(data), "\n") {
			w.Write([]byte("<tr>"))
			for _, cell := range strings.Split(line, ",") {
				w.Write([]byte("<td>" + template_htmlEscape(cell) + "</td>"))
			}
			w.Write([]byte("</tr>"))
		}
		w.Write([]byte("</table>"))
		return
	}

	if asAttachment {
		w.Header().Set("Content-Disposition", `attachment; filename="`+att.Filename+`"`)
	}
	w.Header().Set("Content-Type", contentType)
	http.ServeFile(w, r, att.FilePath)
}

// attachmentAccessible checks the scoped-admin domain restriction against the parent
// email log's sender domain; writes 404 and returns false if disallowed.
func (a *App) attachmentAccessible(w http.ResponseWriter, r *http.Request, att *db.EmailAttachment) bool {
	log, err := a.DB.GetEmailLogByID(att.EmailLogID)
	if err != nil || log == nil {
		http.NotFound(w, r)
		return false
	}
	if ok, err := a.emailLogAccessible(r, log.MailFrom); err != nil || !ok {
		http.NotFound(w, r)
		return false
	}
	return true
}

func extOfName(filename string) string {
	if i := strings.LastIndex(filename, "."); i >= 0 {
		return strings.ToLower(filename[i:])
	}
	return ""
}

func template_htmlEscape(s string) string {
	r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
	return r.Replace(s)
}

// deleteAttachment mirrors view_message.py's delete_attachment(): accepts GET or POST.
func (a *App) deleteAttachment(w http.ResponseWriter, r *http.Request) {
	att, err := a.DB.GetAttachmentByID(pathID(r))
	if err != nil || att == nil {
		http.NotFound(w, r)
		return
	}
	if !a.attachmentAccessible(w, r, att) {
		return
	}
	os.Remove(att.FilePath)
	if err := a.DB.RemoveAttachment(att.ID); err != nil {
		setFlash(w, "error", "Error deleting attachment")
	} else {
		setFlash(w, "success", "Attachment deleted")
	}
	http.Redirect(w, r, Prefix+"/logs?type=emails", http.StatusFound)
}
