package mailruntime

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

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/token"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type getAttachmentInput struct {
	Attachment string `json:"attachment"`
	Target     string `json:"target"`
}

type getAttachmentOutput struct {
	Status                   string `json:"status"`
	AttachmentID             string `json:"attachmentId,omitempty"`
	Filename                 string `json:"filename,omitempty"`
	MediaType                string `json:"mediaType,omitempty"`
	SizeBytes                int    `json:"sizeBytes,omitempty"`
	Truncated                bool   `json:"truncated"`
	UntrustedExternalContent bool   `json:"untrustedExternalContent"`
}

// getAttachment reads the exact bounded MIME part behind a sealed attachment
// reference and materializes it as a AgentTeam attachment in the invoking Agent's
// single authorized conversation target. It never returns attachment bytes in
// the Builtin result; the Action output carries only the AgentTeam attachment ID.
func (service *Service) getAttachment(
	ctx context.Context,
	invocation Invocation,
) (json.RawMessage, error) {
	var input getAttachmentInput
	if err := decodeStrictJSON(invocation.Input, &input); err != nil ||
		input.Attachment == "" {
		return nil, mailError(
			ErrorValidation,
			"mail attachment input is invalid",
			err,
		)
	}
	if service.materializer == nil {
		return nil, mailError(
			ErrorPermissionDenied,
			"mail attachment materialization is unavailable",
			nil,
		)
	}
	runtime, err := service.resolveRuntime(ctx, invocation)
	if err != nil {
		return nil, err
	}
	ref, claims, err := service.openReference(
		input.Attachment,
		runtime,
		token.KindAttachment,
		actionGetAttachment,
	)
	if err != nil {
		return nil, err
	}
	part, err := parseAttachmentPart(claims.Part)
	if err != nil {
		return nil, mailError(
			ErrorValidation,
			"mail attachment reference is invalid",
			err,
		)
	}
	runtime, err = service.recheckRuntime(ctx, runtime)
	if err != nil {
		return nil, err
	}
	content, err := service.imap.GetAttachment(
		ctx,
		runtime.material.IMAP,
		ref,
		part,
	)
	if err != nil {
		return nil, classifyProtocolError(err)
	}
	if _, err := service.recheckRuntime(ctx, runtime); err != nil {
		return nil, err
	}
	target := strings.TrimSpace(input.Target)
	if target == "" {
		return nil, mailError(ErrorValidation, "mail attachment target is required", nil)
	}
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return nil, err
	}
	materialized, err := service.materializer.MaterializeAgentAttachment(
		ctx,
		invocation.WorkspaceID,
		invocation.AgentID,
		invocation.AgentMemberID,
		target,
		AttachmentUpload{
			Filename:  content.Filename,
			MediaType: content.MediaType,
			Blob:      content.Blob,
		},
	)
	if err != nil {
		classified := classifyMaterializerError(err)
		if code, valid := ErrorCodeOf(classified); valid && code == ErrorUpstream {
			// The upload may already be committed; retrying would duplicate
			// it. Report unknown without the attachment ID or any retry hint.
			return marshalBounded(unknownAttachmentOutput())
		}
		return nil, classified
	}
	if materialized.AttachmentID == "" ||
		materialized.SizeBytes != int64(len(content.Blob)) {
		// The projection is unusable even though the upload may exist.
		return marshalBounded(unknownAttachmentOutput())
	}
	// The AgentTeam-side mutation already happened; a revocation discovered now
	// can neither undo it nor justify claiming success.
	if err := service.authorizeMutation(ctx, invocation); err != nil {
		return marshalBounded(unknownAttachmentOutput())
	}
	return marshalBounded(getAttachmentOutput{
		Status:                   "materialized",
		AttachmentID:             materialized.AttachmentID,
		Filename:                 content.Filename,
		MediaType:                content.MediaType,
		SizeBytes:                len(content.Blob),
		Truncated:                false,
		UntrustedExternalContent: true,
	})
}

// unknownAttachmentOutput is the conservative result for every window where
// the AgentTeam-side mutation may already have happened: no attachment ID, no
// retry invitation, caller must reread before acting.
func unknownAttachmentOutput() getAttachmentOutput {
	return getAttachmentOutput{
		Status:                   "unknown",
		Truncated:                false,
		UntrustedExternalContent: true,
	}
}

// parseAttachmentPart decodes the dotted IMAP part path sealed in the
// attachment reference claims.
func parseAttachmentPart(encoded string) ([]int, error) {
	trimmed := strings.TrimSpace(encoded)
	if trimmed == "" {
		return nil, attachmentPartError{}
	}
	segments := strings.Split(trimmed, ".")
	if len(segments) > maxAttachmentMetadata {
		return nil, attachmentPartError{}
	}
	part := make([]int, 0, len(segments))
	for _, segment := range segments {
		value, err := strconv.Atoi(segment)
		if err != nil || value < 1 || value > maxAttachmentMetadata {
			return nil, attachmentPartError{}
		}
		part = append(part, value)
	}
	return part, nil
}

type attachmentPartError struct{}

func (attachmentPartError) Error() string {
	return "attachment part path is invalid"
}

func classifyMaterializerError(err error) error {
	switch {
	case err == nil:
		return nil
	case errors.Is(err, storeerr.ErrForbidden),
		errors.Is(err, storeerr.ErrNotFound):
		return mailError(
			ErrorPermissionDenied,
			"mail attachment target is not writable",
			err,
		)
	case errors.Is(err, storeerr.ErrBadRequest),
		errors.Is(err, storeerr.ErrUnprocessable):
		return mailError(
			ErrorValidation,
			"mail attachment target is invalid",
			err,
		)
	default:
		return mailError(
			ErrorUpstream,
			"mail attachment could not be materialized",
			err,
		)
	}
}
