package runtimeapi

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"io"
	"strings"
	"time"

	"github.com/nexuscrm/shared/pkg/models"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	mailboxauthorizationsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/mailboxauthorizationv1"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"github.com/pingcap-inc/nexuscrm-contract/wire-go/mapping"
)

const mailboxAuthorizationTTL = 10 * time.Minute

var (
	errMailboxAuthorizationDenied      = errors.New("runtime mailbox authorization is denied")
	errMailboxAuthorizationUnavailable = errors.New("runtime mailbox authorization is unavailable")
)

type mailboxAuthorizationHandle struct {
	HandleDigest        string
	OrgID               string
	AppInstallationID   string
	AuthorizedSubjectID string
	SessionID           string
	AuthEpoch           uint64
	PermissionFloor     permissionstatev1.Version
	CatalogRevision     uint64
	SecurityEpoch       uint64
	ExpiresAt           time.Time
	ConsumedAt          *time.Time
	CreatedAt           time.Time
}

type mailboxAuthorizationHandleStore interface {
	Mint(context.Context, mailboxAuthorizationHandle, time.Duration) error
	ConsumeAuthorized(
		context.Context,
		string,
		func(context.Context, mailboxAuthorizationHandle) error,
	) (bool, error)
}

// MailboxPermissionRequirement selects exactly one permission namespace.
// Keeping namespaces explicit preserves existing custom-only authorization.
type MailboxPermissionRequirement struct {
	CustomPermission string
	SystemPermission string
}

func (requirement MailboxPermissionRequirement) valid() bool {
	if (requirement.CustomPermission == "") == (requirement.SystemPermission == "") {
		return false
	}
	name := requirement.CustomPermission
	if name == "" {
		name = requirement.SystemPermission
	}
	return len(name) <= 255 && strings.TrimSpace(name) == name && !strings.ContainsAny(name, "\x00\r\n")
}

type mailboxPermissionAuthorizer interface {
	AuthorizeMailboxSubject(
		context.Context,
		permissionstatev1.Subject,
		permissionstatev1.Version,
		MailboxPermissionRequirement,
	) error
	AuthorizeMailboxSubjectWithinTransaction(
		context.Context,
		permissionstatev1.Subject,
		permissionstatev1.Version,
		MailboxPermissionRequirement,
	) error
}

type mailboxAuthorizationSession struct {
	User            models.UserSession
	Subject         permissionstatev1.Subject
	AuthEpoch       uint64
	PermissionFloor permissionstatev1.Version
}

type mailboxSessionReader interface {
	ReadMailboxSession(context.Context, string, string) (mailboxAuthorizationSession, error)
}

// MailboxAuthorizationServer owns the narrow Runtime authorization decision
// for an Integration-managed connected mailbox. It never exposes an effective
// permission snapshot and never accepts provider credentials or provider
// identity from the caller.
type MailboxAuthorizationServer struct {
	runtimev1.UnimplementedConnectedMailboxAuthorizationServiceServer
	store              mailboxAuthorizationHandleStore
	permissions        mailboxPermissionAuthorizer
	sessions           mailboxSessionReader
	requiredPermission MailboxPermissionRequirement
	random             io.Reader
}

func NewMailboxAuthorizationServer(
	store mailboxAuthorizationHandleStore,
	permissions mailboxPermissionAuthorizer,
	sessions mailboxSessionReader,
	requiredPermission MailboxPermissionRequirement,
) (*MailboxAuthorizationServer, error) {
	if store == nil || permissions == nil || sessions == nil || !requiredPermission.valid() {
		return nil, errMailboxAuthorizationUnavailable
	}
	return &MailboxAuthorizationServer{
		store: store, permissions: permissions, sessions: sessions,
		requiredPermission: requiredPermission, random: rand.Reader,
	}, nil
}

func (server *MailboxAuthorizationServer) AuthorizeConnectedMailbox(
	ctx context.Context,
	wireRequest *runtimev1.AuthorizeConnectedMailboxRequest,
) (*runtimev1.AuthorizeConnectedMailboxResponse, error) {
	request, err := mapping.FromWireAuthorizeConnectedMailboxRequest(ctx, wireRequest)
	if err != nil {
		return nil, mailboxAuthorizationStatus(ctx, err)
	}
	scope, scopeOK := runtimeExecutionScopeFromContext(ctx)
	trusted, trustedOK := trustedCallFromContext(ctx)
	if !scopeOK || !trustedOK || trusted.subject.Validate() != nil ||
		scope.orgID != trusted.orgID ||
		scope.appInstallationID != trusted.appInstallationID ||
		scope.workloadID != trusted.callerWorkloadID ||
		scope.capability != mailboxauthorizationsemantic.CapabilityName ||
		scope.receipts.SecurityEpoch != trusted.securityEpoch {
		return nil, mailboxAuthorizationStatus(ctx, errMailboxAuthorizationDenied)
	}
	if err := requireCatalogRevision(
		request.MinCatalogRevision,
		scope.receipts.CatalogRevision,
	); err != nil {
		return nil, mailboxAuthorizationStatus(ctx, err)
	}
	if request.Operation == mailboxauthorizationsemantic.OperationBegin {
		if err := server.permissions.AuthorizeMailboxSubject(
			ctx,
			trusted.subject,
			trusted.permissionFloor,
			server.requiredPermission,
		); err != nil {
			return nil, mailboxAuthorizationStatus(ctx, err)
		}
	}
	response := mailboxauthorizationsemantic.Response{
		AuthorizedSubjectID: trusted.user.ID,
		Receipts: receiptsForCapability(
			scope.receipts,
			mailboxauthorizationsemantic.CapabilityName,
		),
	}
	if request.Operation == mailboxauthorizationsemantic.OperationBegin {
		handle, err := randomMailboxAuthorizationHandle(server.random)
		if err != nil {
			return nil, mailboxAuthorizationStatus(ctx, errMailboxAuthorizationUnavailable)
		}
		if err := server.store.Mint(ctx, mailboxAuthorizationHandle{
			HandleDigest: digestMailboxAuthorizationHandle(handle),
			OrgID:        scope.orgID, AppInstallationID: scope.appInstallationID,
			AuthorizedSubjectID: trusted.user.ID, SessionID: trusted.sessionID,
			AuthEpoch: trusted.authEpoch, PermissionFloor: trusted.permissionFloor,
			CatalogRevision: scope.receipts.CatalogRevision,
			SecurityEpoch:   trusted.securityEpoch,
		}, mailboxAuthorizationTTL); err != nil {
			return nil, mailboxAuthorizationStorageStatus(ctx, err)
		}
		response.ReauthorizationHandle = handle
	}
	wireResponse, err := mapping.ToWireAuthorizeConnectedMailboxResponse(request, response)
	if err != nil {
		return nil, mailboxAuthorizationStatus(ctx, err)
	}
	return wireResponse, nil
}

func (server *MailboxAuthorizationServer) ReauthorizeConnectedMailbox(
	ctx context.Context,
	wireRequest *runtimev1.ReauthorizeConnectedMailboxRequest,
) (*runtimev1.ReauthorizeConnectedMailboxResponse, error) {
	request, err := mapping.FromWireReauthorizeConnectedMailboxRequest(ctx, wireRequest)
	if err != nil {
		return nil, mailboxAuthorizationStatus(ctx, err)
	}
	scope, scopeOK := runtimeExecutionScopeFromContext(ctx)
	workload, workloadOK := trustedWorkloadCallFromContext(ctx)
	if !scopeOK || !workloadOK ||
		scope.orgID != workload.orgID ||
		scope.workloadID != workload.callerWorkloadID ||
		scope.capability != mailboxauthorizationsemantic.CapabilityName ||
		workload.capability != mailboxauthorizationsemantic.CapabilityName ||
		scope.receipts.SecurityEpoch != workload.securityEpoch {
		return nil, mailboxAuthorizationStatus(ctx, errMailboxAuthorizationDenied)
	}
	if err := requireCatalogRevision(
		request.MinCatalogRevision,
		scope.receipts.CatalogRevision,
	); err != nil {
		return nil, mailboxAuthorizationStatus(ctx, err)
	}
	digest := digestMailboxAuthorizationHandle(request.ReauthorizationHandle)
	var authorizedSubjectID string
	_, err = server.store.ConsumeAuthorized(
		ctx,
		digest,
		func(txCtx context.Context, record mailboxAuthorizationHandle) error {
			if !validMailboxAuthorizationHandle(record) || record.HandleDigest != digest ||
				record.OrgID != scope.orgID ||
				record.AppInstallationID != scope.appInstallationID ||
				record.CatalogRevision != scope.receipts.CatalogRevision ||
				record.SecurityEpoch != workload.securityEpoch {
				return errMailboxAuthorizationDenied
			}
			session, err := server.sessions.ReadMailboxSession(
				txCtx,
				record.SessionID,
				record.AuthorizedSubjectID,
			)
			if err != nil {
				return err
			}
			if session.AuthEpoch != record.AuthEpoch ||
				session.User.ID != record.AuthorizedSubjectID ||
				session.Subject.Validate() != nil ||
				session.Subject.UserID() != record.AuthorizedSubjectID ||
				session.Subject.ProfileID() != session.User.ProfileID {
				return errMailboxAuthorizationDenied
			}
			comparison, err := session.PermissionFloor.Compare(record.PermissionFloor)
			if err != nil || comparison != 0 {
				return errMailboxAuthorizationUnavailable
			}
			if err := server.permissions.AuthorizeMailboxSubjectWithinTransaction(
				txCtx,
				session.Subject,
				record.PermissionFloor,
				server.requiredPermission,
			); err != nil {
				return err
			}
			authorizedSubjectID = record.AuthorizedSubjectID
			return nil
		},
	)
	if err != nil {
		return nil, mailboxAuthorizationStorageStatus(ctx, err)
	}
	if authorizedSubjectID == "" {
		return nil, mailboxAuthorizationStatus(ctx, errMailboxAuthorizationUnavailable)
	}
	response := mailboxauthorizationsemantic.Response{
		AuthorizedSubjectID: authorizedSubjectID,
		Receipts: receiptsForCapability(
			scope.receipts,
			mailboxauthorizationsemantic.CapabilityName,
		),
	}
	wireResponse, err := mapping.ToWireReauthorizeConnectedMailboxResponse(request, response)
	if err != nil {
		return nil, mailboxAuthorizationStatus(ctx, err)
	}
	return wireResponse, nil
}

func randomMailboxAuthorizationHandle(source io.Reader) (string, error) {
	if source == nil {
		return "", errMailboxAuthorizationUnavailable
	}
	raw := make([]byte, 32)
	if _, err := io.ReadFull(source, raw); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(raw), nil
}

func digestMailboxAuthorizationHandle(handle string) string {
	sum := sha256.Sum256([]byte(handle))
	return hex.EncodeToString(sum[:])
}

func mailboxAuthorizationStatus(ctx context.Context, err error) error {
	if ctx != nil && ctx.Err() != nil {
		return mapping.ToPlatformStatusError(ctx.Err(), mailboxauthorizationsemantic.CapabilityName)
	}
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return mapping.ToPlatformStatusError(err, mailboxauthorizationsemantic.CapabilityName)
	}
	if errors.Is(err, errMailboxAuthorizationDenied) {
		return mapping.ToPlatformStatusError(
			&mailboxauthorizationsemantic.Failure{Category: commonsemantic.FailureForbidden},
			mailboxauthorizationsemantic.CapabilityName,
		)
	}
	if errors.Is(err, errMailboxAuthorizationUnavailable) {
		return mapping.ToPlatformStatusError(
			&mailboxauthorizationsemantic.Failure{Category: commonsemantic.FailureUnavailable},
			mailboxauthorizationsemantic.CapabilityName,
		)
	}
	return mapping.ToPlatformStatusError(err, mailboxauthorizationsemantic.CapabilityName)
}

func mailboxAuthorizationStorageStatus(ctx context.Context, err error) error {
	if ctx != nil && ctx.Err() != nil {
		return mailboxAuthorizationStatus(ctx, ctx.Err())
	}
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return mailboxAuthorizationStatus(ctx, err)
	}
	if errors.Is(err, errMailboxAuthorizationDenied) {
		return mailboxAuthorizationStatus(ctx, errMailboxAuthorizationDenied)
	}
	return mailboxAuthorizationStatus(ctx, errMailboxAuthorizationUnavailable)
}

var _ runtimev1.ConnectedMailboxAuthorizationServiceServer = (*MailboxAuthorizationServer)(nil)
