package webui

import (
	"bytes"
	"crypto/x509"
	"encoding/pem"
	"io"
	"mime"
	"net/http"
	"net/mail"
	"strings"

	"mailgoserver/internal/db"
	"mailgoserver/internal/pgp"
	"mailgoserver/internal/smime"
)

// smimeInfo summarizes what unwrapSMIME found, for the message view to show as
// badges. A message can be both signed and encrypted (either nesting order); the
// zero value means "plain, no S/MIME involved."
type smimeInfo struct {
	Signed       bool
	SignatureOK  bool
	SignatureErr string
	SignerEmail  string
	Encrypted    bool
	Decrypted    bool
	DecryptErr   string
}

// pgpInfo summarizes what unwrapCrypto found about PGP encryption — parallel to
// smimeInfo, but PGP has no signing role in this codebase (S/MIME handles that), so
// there's no Signed/SignatureOK equivalent here.
type pgpInfo struct {
	Encrypted   bool
	Decrypted   bool
	DecryptErr  string
	NeedsUnlock bool
	Identities  []db.MailboxPGPIdentity
}

// entityHeaderNames are the only headers that belong to a MIME entity (as opposed to
// the message envelope) — see smime.Entity's doc comment.
var entityHeaderNames = []string{"Content-Type", "Content-Transfer-Encoding", "Content-Disposition"}

func isEntityHeader(name string) bool {
	for _, n := range entityHeaderNames {
		if strings.EqualFold(n, name) {
			return true
		}
	}
	return false
}

// tryDecryptWithIdentities attempts a pkcs7-mime decrypt using every S/MIME
// identity this mailbox holds — deliberately not inspecting the CMS RecipientInfo
// to figure out which identity a message targets; trying each key against
// smime.Decrypt is cheap and simple, and a mailbox realistically holds only a
// handful of identities. No passphrase/unlock step: S/MIME keys are stored plain.
func tryDecryptWithIdentities(entity smime.Entity, identities []db.MailboxSMIMEIdentity) (smime.Entity, bool) {
	for _, id := range identities {
		cert, err := smime.ParseCertPEM([]byte(id.CertPEM))
		if err != nil {
			continue
		}
		key, err := smime.ParseKeyPEM([]byte(id.KeyPEM))
		if err != nil {
			continue
		}
		if inner, err := smime.Decrypt(entity, cert, key); err == nil {
			return inner, true
		}
	}
	return smime.Entity{}, false
}

// tryPGPDecryptWithUnlockedIdentities mirrors tryDecryptWithUnlockedIdentities for
// PGP: tries every identity already unlocked in this session's key cache.
func (a *App) tryPGPDecryptWithUnlockedIdentities(token string, entity pgp.Entity, identities []db.MailboxPGPIdentity) (pgp.Entity, bool) {
	for _, id := range identities {
		unlocked, ok := a.pgpKeys.get(token, id.ID)
		if !ok {
			continue
		}
		if inner, err := pgp.DecryptEntity(entity, unlocked); err == nil {
			return inner, true
		}
	}
	return pgp.Entity{}, false
}

// unwrapCrypto strips any S/MIME signing and/or PGP encryption layers off raw (in
// whichever order they were applied — sign-then-encrypt or encrypt-then-sign, and
// either protocol can be outermost), returning a flat RFC822 message with the
// original envelope headers and the innermost plaintext MIME entity, ready for
// mailview.Parse. A verified S/MIME signature also gets the signer's certificate
// auto-captured into the mailbox's S/MIME contacts, the same way any mail client
// would on receiving a good signature (PGP has no signing role here, so no
// equivalent auto-capture on that side). Never returns an error for "this isn't
// S/MIME or PGP" — that's the common case, and raw is returned unchanged.
func (a *App) unwrapCrypto(r *http.Request, mailboxID int64, raw []byte) ([]byte, smimeInfo, pgpInfo) {
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return raw, smimeInfo{}, pgpInfo{}
	}
	body, err := io.ReadAll(msg.Body)
	if err != nil {
		return raw, smimeInfo{}, pgpInfo{}
	}

	entity := smime.Entity{Body: body}
	for _, name := range entityHeaderNames {
		if v := msg.Header.Get(name); v != "" {
			entity.Headers = append(entity.Headers, name+": "+v)
		}
	}

	var sInfo smimeInfo
	var pInfo pgpInfo
	var envelopeHeaders []string
	for key, vals := range msg.Header {
		if isEntityHeader(key) {
			continue
		}
		for _, v := range vals {
			envelopeHeaders = append(envelopeHeaders, key+": "+v)
		}
	}

unwrap:
	for i := 0; i < 5; i++ { // cap against pathological nesting; real S/MIME/PGP never nests this deep
		ct := smime.HeaderValue(entity.Headers, "Content-Type")
		mediaType := strings.ToLower(strings.TrimSpace(strings.SplitN(ct, ";", 2)[0]))
		switch mediaType {
		case "multipart/signed":
			sInfo.Signed = true
			inner, signer, verr := smime.VerifySigned(entity)
			if verr != nil {
				sInfo.SignatureErr = verr.Error()
			} else {
				sInfo.SignatureOK = true
				if len(signer.EmailAddresses) > 0 {
					sInfo.SignerEmail = signer.EmailAddresses[0]
					a.captureSignerContact(mailboxID, sInfo.SignerEmail, signer)
				}
			}
			entity = inner
		case "application/pkcs7-mime":
			sInfo.Encrypted = true
			identities, ierr := a.DB.ListSMIMEIdentities(mailboxID)
			if ierr != nil {
				sInfo.DecryptErr = ierr.Error()
				break unwrap
			}
			if inner, ok := tryDecryptWithIdentities(entity, identities); ok {
				sInfo.Decrypted = true
				entity = inner
			} else {
				sInfo.DecryptErr = "no S/MIME certificate on file could decrypt this message"
				break unwrap
			}
		case "multipart/encrypted":
			_, params, perr := mime.ParseMediaType(ct)
			if perr != nil || !strings.EqualFold(params["protocol"], "application/pgp-encrypted") {
				break unwrap
			}
			pInfo.Encrypted = true
			pgpIdentities, ierr := a.DB.ListPGPIdentities(mailboxID)
			if ierr != nil {
				pInfo.DecryptErr = ierr.Error()
				break unwrap
			}
			if inner, ok := a.tryPGPDecryptWithUnlockedIdentities(sessionToken(r), pgp.Entity(entity), pgpIdentities); ok {
				pInfo.Decrypted = true
				entity = smime.Entity(inner)
			} else if len(pgpIdentities) == 0 {
				pInfo.DecryptErr = "no PGP key to decrypt with — set one up on the Certs page"
				break unwrap
			} else {
				pInfo.NeedsUnlock = true
				pInfo.Identities = pgpIdentities
				break unwrap
			}
		default:
			break unwrap
		}
	}

	var buf bytes.Buffer
	for _, h := range envelopeHeaders {
		buf.WriteString(h)
		buf.WriteString("\r\n")
	}
	for _, h := range entity.Headers {
		buf.WriteString(h)
		buf.WriteString("\r\n")
	}
	buf.WriteString("\r\n")
	buf.Write(entity.Body)
	return buf.Bytes(), sInfo, pInfo
}

func (a *App) captureSignerContact(mailboxID int64, email string, cert *x509.Certificate) {
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
	if err := a.DB.UpsertSMIMEContact(mailboxID, email, string(certPEM)); err != nil {
		a.Logger.Error("auto-capture signer cert for mailbox %d: %v", mailboxID, err)
	}
}
