package app

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"
	"time"

	"momswap/backend/internal/auth"
	"momswap/backend/internal/store"
)

var (
	ErrUnauthorized              = errors.New("unauthorized")
	ErrForbidden                 = errors.New("forbidden")
	ErrBadRequest                = errors.New("bad request")
	ErrRegistrationNotConfigured = errors.New("registration by signature not configured")
	ErrInviteInvalid             = errors.New("invite invalid")
	ErrInviteExpired             = errors.New("invite expired")
	ErrInviteExhaust             = errors.New("invite exhausted")
	ErrAlreadyUser               = errors.New("user already registered")
	ErrCollectionMiss            = errors.New("collection missing")
	ErrFeatureMiss               = errors.New("feature missing")
	ErrAssetMiss                 = errors.New("asset missing")
	ErrStorageNotConfigured      = errors.New("storage not configured")
)

type Config struct {
	ChallengeTTL time.Duration
	SessionTTL   time.Duration
	UploadURLTTL time.Duration
	ReadURLTTL   time.Duration
}

type AssetURLSigner interface {
	SignedGetObjectURL(ctx context.Context, objectKey string, expiry time.Duration) (string, error)
	PutObject(ctx context.Context, objectKey, contentType string, body io.Reader, size int64) error
	GetObject(ctx context.Context, objectKey string) (io.ReadCloser, string, int64, error)
}

type Service struct {
	store            store.Store
	config           Config
	servicePublicKey string
	assetSigner      AssetURLSigner
}

func NewService(st store.Store, cfg Config, servicePublicKey string) *Service {
	if cfg.UploadURLTTL <= 0 {
		cfg.UploadURLTTL = 15 * time.Minute
	}
	if cfg.ReadURLTTL <= 0 {
		cfg.ReadURLTTL = 10 * time.Minute
	}
	return &Service{store: st, config: cfg, servicePublicKey: servicePublicKey}
}

func (s *Service) ConfigureAssetStorage(signer AssetURLSigner) {
	s.assetSigner = signer
}

type InvitationPayload struct {
	JTI              string `json:"jti"`
	InviterPublicKey string `json:"inviterPublicKey"`
	InviteePublicKey string `json:"inviteePublicKey,omitempty"`
	ExpiresAtUnix    int64  `json:"expiresAtUnix"`
	MaxUses          int    `json:"maxUses"`
}

func (s *Service) BootstrapAdmin(publicKey string) {
	if publicKey == "" {
		return
	}
	s.store.UpsertUser(store.User{
		PublicKey: publicKey,
		CreatedAt: time.Now().UTC(),
	})
}

func (s *Service) ServicePublicKey() string {
	return s.servicePublicKey
}

func (s *Service) RegisterBySignature(publicKey, signature string) error {
	if s.servicePublicKey == "" {
		return ErrRegistrationNotConfigured
	}
	if publicKey == "" {
		return fmt.Errorf("%w: missing public key", ErrBadRequest)
	}
	if _, err := s.store.GetUser(publicKey); err == nil {
		return ErrAlreadyUser
	}
	if err := auth.VerifySignature(publicKey, s.servicePublicKey, signature); err != nil {
		return fmt.Errorf("%w: signature verification failed", ErrBadRequest)
	}
	s.store.UpsertUser(store.User{
		PublicKey: publicKey,
		CreatedAt: time.Now().UTC(),
	})
	return nil
}

func (s *Service) CreateChallenge(publicKey, clientIP string) (string, error) {
	if publicKey == "" {
		return "", fmt.Errorf("%w: missing public key", ErrBadRequest)
	}

	nonce, err := auth.NewRandomToken(24)
	if err != nil {
		return "", err
	}
	err = s.store.CreateChallenge(store.Challenge{
		Nonce:     nonce,
		PublicKey: publicKey,
		IP:        clientIP,
		ExpiresAt: time.Now().UTC().Add(s.config.ChallengeTTL),
		Used:      false,
	})
	if err != nil {
		return "", err
	}
	return nonce, nil
}

func (s *Service) Login(publicKey, nonce, signature, clientIP string) (string, error) {
	ch, err := s.store.GetChallenge(nonce)
	if err != nil {
		return "", fmt.Errorf("%w: challenge not found", ErrUnauthorized)
	}
	if ch.Used || time.Now().UTC().After(ch.ExpiresAt) {
		return "", fmt.Errorf("%w: challenge expired", ErrUnauthorized)
	}
	if ch.PublicKey != publicKey {
		return "", fmt.Errorf("%w: challenge key mismatch", ErrUnauthorized)
	}
	if _, err := s.store.GetUser(publicKey); err != nil {
		return "", fmt.Errorf("%w: user not registered", ErrUnauthorized)
	}

	msg := "login:" + nonce
	if err := auth.VerifySignature(publicKey, msg, signature); err != nil {
		return "", fmt.Errorf("%w: %v", ErrUnauthorized, err)
	}
	_ = s.store.MarkChallengeUsed(nonce)

	token, err := auth.NewRandomToken(32)
	if err != nil {
		return "", err
	}
	s.store.SaveSession(store.Session{
		Token:     token,
		PublicKey: publicKey,
		ExpiresAt: time.Now().UTC().Add(s.config.SessionTTL),
	})
	s.store.SaveUserLogin(store.UserLogin{
		PublicKey: publicKey,
		IP:        clientIP,
		CreatedAt: time.Now().UTC(),
	})
	return token, nil
}

func (s *Service) AuthenticateSession(token string) (string, error) {
	session, err := s.store.GetSession(token)
	if err != nil || time.Now().UTC().After(session.ExpiresAt) {
		return "", ErrUnauthorized
	}
	return session.PublicKey, nil
}

func (s *Service) CreateInvitation(authenticatedKey, invitePayloadB64, inviteSigB64 string) error {
	payloadRaw, err := base64.RawURLEncoding.DecodeString(invitePayloadB64)
	if err != nil {
		return fmt.Errorf("%w: decode payload", ErrBadRequest)
	}

	var payload InvitationPayload
	if err := json.Unmarshal(payloadRaw, &payload); err != nil {
		return fmt.Errorf("%w: invalid payload json", ErrBadRequest)
	}
	if payload.InviterPublicKey != authenticatedKey {
		return fmt.Errorf("%w: inviter mismatch", ErrForbidden)
	}
	if payload.JTI == "" || payload.MaxUses <= 0 || payload.ExpiresAtUnix <= 0 {
		return fmt.Errorf("%w: invalid invite fields", ErrBadRequest)
	}
	if time.Now().UTC().After(time.Unix(payload.ExpiresAtUnix, 0).UTC()) {
		return ErrInviteExpired
	}
	if _, err := s.store.GetUser(payload.InviterPublicKey); err != nil {
		return fmt.Errorf("%w: inviter user missing", ErrForbidden)
	}

	if err := auth.VerifySignature(payload.InviterPublicKey, "invite:"+invitePayloadB64, inviteSigB64); err != nil {
		return fmt.Errorf("%w: %v", ErrBadRequest, err)
	}

	return s.store.SaveInvitation(store.Invitation{
		JTI:              payload.JTI,
		InviterPublicKey: payload.InviterPublicKey,
		InviteePublicKey: payload.InviteePublicKey,
		ExpiresAt:        time.Unix(payload.ExpiresAtUnix, 0).UTC(),
		MaxUses:          payload.MaxUses,
		UsedCount:        0,
	})
}

func (s *Service) Register(newPublicKey, invitePayloadB64, inviteSigB64, proofSigB64 string) error {
	if newPublicKey == "" {
		return fmt.Errorf("%w: missing public key", ErrBadRequest)
	}
	if _, err := s.store.GetUser(newPublicKey); err == nil {
		return ErrAlreadyUser
	}

	payloadRaw, err := base64.RawURLEncoding.DecodeString(invitePayloadB64)
	if err != nil {
		return fmt.Errorf("%w: decode payload", ErrBadRequest)
	}
	var payload InvitationPayload
	if err := json.Unmarshal(payloadRaw, &payload); err != nil {
		return fmt.Errorf("%w: invalid payload", ErrBadRequest)
	}

	if err := auth.VerifySignature(payload.InviterPublicKey, "invite:"+invitePayloadB64, inviteSigB64); err != nil {
		return fmt.Errorf("%w: invite signature", ErrInviteInvalid)
	}
	if _, err := s.store.GetUser(payload.InviterPublicKey); err != nil {
		return fmt.Errorf("%w: inviter missing", ErrInviteInvalid)
	}
	inv, err := s.store.GetInvitation(payload.JTI)
	if err != nil {
		return fmt.Errorf("%w: invite unknown", ErrInviteInvalid)
	}
	if inv.InviterPublicKey != payload.InviterPublicKey {
		return fmt.Errorf("%w: inviter mismatch", ErrInviteInvalid)
	}
	if time.Now().UTC().After(inv.ExpiresAt) {
		return ErrInviteExpired
	}
	if inv.UsedCount >= inv.MaxUses {
		return ErrInviteExhaust
	}
	if inv.InviteePublicKey != "" && inv.InviteePublicKey != newPublicKey {
		return fmt.Errorf("%w: invite bound to another key", ErrInviteInvalid)
	}

	proofMessage := "register:" + newPublicKey + ":" + payload.JTI
	if err := auth.VerifySignature(newPublicKey, proofMessage, proofSigB64); err != nil {
		return fmt.Errorf("%w: register proof", ErrInviteInvalid)
	}

	if err := s.store.IncrementInvitationUsage(payload.JTI); err != nil {
		return err
	}
	s.store.UpsertUser(store.User{
		PublicKey: newPublicKey,
		Inviter:   payload.InviterPublicKey,
		CreatedAt: time.Now().UTC(),
	})
	return nil
}

func validatePoint(point store.Point) error {
	if point.Type != "Point" {
		return fmt.Errorf("%w: geometry type must be Point", ErrBadRequest)
	}
	if len(point.Coordinates) != 2 && len(point.Coordinates) != 3 {
		return fmt.Errorf("%w: coordinates must have lon/lat[/alt]", ErrBadRequest)
	}
	lon, lat := point.Coordinates[0], point.Coordinates[1]
	if lon < -180 || lon > 180 {
		return fmt.Errorf("%w: longitude must be -180 to 180, got %.2f", ErrBadRequest, lon)
	}
	if lat < -90 || lat > 90 {
		return fmt.Errorf("%w: latitude must be -90 to 90, got %.2f", ErrBadRequest, lat)
	}
	return nil
}

func (s *Service) CreateCollection(ownerKey, name string) (store.Collection, error) {
	if name == "" {
		return store.Collection{}, fmt.Errorf("%w: collection name required", ErrBadRequest)
	}
	id, err := auth.NewRandomToken(12)
	if err != nil {
		return store.Collection{}, err
	}
	c := store.Collection{
		ID:        id,
		OwnerKey:  ownerKey,
		Name:      name,
		CreatedAt: time.Now().UTC(),
	}
	s.store.SaveCollection(c)
	return c, nil
}

func (s *Service) ListCollections(ownerKey string) []store.Collection {
	return s.store.ListCollectionsByOwner(ownerKey)
}

func (s *Service) UpdateCollection(ownerKey, collectionID, name string) (store.Collection, error) {
	collection, err := s.store.GetCollection(collectionID)
	if err != nil {
		return store.Collection{}, ErrCollectionMiss
	}
	if collection.OwnerKey != ownerKey {
		return store.Collection{}, ErrForbidden
	}
	if name == "" {
		return store.Collection{}, fmt.Errorf("%w: collection name required", ErrBadRequest)
	}
	collection.Name = name
	s.store.SaveCollection(collection)
	return collection, nil
}

func (s *Service) DeleteCollection(ownerKey, collectionID string) error {
	collection, err := s.store.GetCollection(collectionID)
	if err != nil {
		return ErrCollectionMiss
	}
	if collection.OwnerKey != ownerKey {
		return ErrForbidden
	}
	return s.store.DeleteCollection(collectionID)
}

func (s *Service) CreateFeature(ownerKey, collectionID string, geometry store.Point, properties map[string]interface{}) (store.Feature, error) {
	collection, err := s.store.GetCollection(collectionID)
	if err != nil {
		return store.Feature{}, ErrCollectionMiss
	}
	if collection.OwnerKey != ownerKey {
		return store.Feature{}, ErrForbidden
	}
	if err := validatePoint(geometry); err != nil {
		return store.Feature{}, err
	}

	id, err := auth.NewRandomToken(12)
	if err != nil {
		return store.Feature{}, err
	}
	now := time.Now().UTC()
	feature := store.Feature{
		ID:           id,
		CollectionID: collectionID,
		OwnerKey:     ownerKey,
		Type:         "Feature",
		Geometry:     geometry,
		Properties:   properties,
		CreatedAt:    now,
		UpdatedAt:    now,
	}
	s.store.SaveFeature(feature)
	return feature, nil
}

func (s *Service) ListFeatures(ownerKey, collectionID string) ([]store.Feature, error) {
	collection, err := s.store.GetCollection(collectionID)
	if err != nil {
		return nil, ErrCollectionMiss
	}
	if collection.OwnerKey != ownerKey {
		return nil, ErrForbidden
	}
	features := s.store.ListFeaturesByCollection(collectionID)
	for idx := range features {
		featureAssets := s.store.ListAssetsByFeature(features[idx].ID)
		assets := make([]map[string]interface{}, 0, len(featureAssets))
		for _, linkedAsset := range featureAssets {
			assets = append(assets, map[string]interface{}{
				"id":          linkedAsset.ID,
				"kind":        linkedAsset.Kind,
				"name":        linkedAsset.Name,
				"description": linkedAsset.Description,
				"checksum":    linkedAsset.Checksum,
				"ext":         linkedAsset.Ext,
				"isPublic":    linkedAsset.IsPublic,
				"link":        "/v1/assets/" + linkedAsset.ID + "/download",
			})
		}
		if features[idx].Properties == nil {
			features[idx].Properties = map[string]interface{}{}
		}
		features[idx].Properties["assets"] = assets
	}
	return features, nil
}

func (s *Service) ListPublicFeatures(kind string) []store.Feature {
	filterKind := strings.TrimSpace(strings.ToLower(kind))
	features := s.store.ListFeaturesAll()
	result := make([]store.Feature, 0, len(features))
	for idx := range features {
		featureAssets := s.store.ListAssetsByFeature(features[idx].ID)
		assets := make([]map[string]interface{}, 0, len(featureAssets))
		for _, linkedAsset := range featureAssets {
			if !linkedAsset.IsPublic {
				continue
			}
			if filterKind != "" && linkedAsset.Kind != filterKind {
				continue
			}
			assets = append(assets, map[string]interface{}{
				"id":          linkedAsset.ID,
				"kind":        linkedAsset.Kind,
				"name":        linkedAsset.Name,
				"description": linkedAsset.Description,
				"checksum":    linkedAsset.Checksum,
				"ext":         linkedAsset.Ext,
				"isPublic":    linkedAsset.IsPublic,
				"link":        "/v1/assets/" + linkedAsset.ID + "/download",
			})
		}
		if len(assets) == 0 {
			continue
		}
		if features[idx].Properties == nil {
			features[idx].Properties = map[string]interface{}{}
		}
		features[idx].Properties["assets"] = assets
		result = append(result, features[idx])
	}
	return result
}

func (s *Service) DeleteFeature(ownerKey, featureID string) error {
	feature, err := s.store.GetFeature(featureID)
	if err != nil {
		return ErrFeatureMiss
	}
	if feature.OwnerKey != ownerKey {
		return ErrForbidden
	}
	return s.store.DeleteFeature(featureID)
}

func (s *Service) UpdateFeatureGeometry(ownerKey, featureID string, geometry store.Point) (store.Feature, error) {
	feature, err := s.store.GetFeature(featureID)
	if err != nil {
		return store.Feature{}, ErrFeatureMiss
	}
	if feature.OwnerKey != ownerKey {
		return store.Feature{}, ErrForbidden
	}
	if err := validatePoint(geometry); err != nil {
		return store.Feature{}, err
	}
	feature.Geometry = geometry
	feature.UpdatedAt = time.Now().UTC()
	s.store.SaveFeature(feature)
	return feature, nil
}

type CreateAssetInput struct {
	FeatureID   string
	Checksum    string
	Ext         string
	Kind        string
	MimeType    string
	SizeBytes   int64
	Name        string
	Description string
	Visibility  *bool
}

func normalizeExt(ext string) string {
	return strings.TrimPrefix(strings.ToLower(strings.TrimSpace(ext)), ".")
}

func normalizeChecksum(checksum string) string {
	return strings.ToLower(strings.TrimSpace(checksum))
}

func (s *Service) CreateOrLinkAsset(ownerKey string, in CreateAssetInput) (store.Asset, bool, error) {
	feature, err := s.store.GetFeature(in.FeatureID)
	if err != nil {
		return store.Asset{}, false, ErrFeatureMiss
	}
	if feature.OwnerKey != ownerKey {
		return store.Asset{}, false, ErrForbidden
	}

	checksum := normalizeChecksum(in.Checksum)
	ext := normalizeExt(in.Ext)
	if checksum == "" || ext == "" {
		return store.Asset{}, false, fmt.Errorf("%w: checksum and ext required", ErrBadRequest)
	}
	switch ext {
	case "jpg", "jpeg", "png", "webp", "gltf", "glb":
	default:
		return store.Asset{}, false, fmt.Errorf("%w: unsupported extension", ErrBadRequest)
	}
	if in.Kind != "image" && in.Kind != "3d" {
		return store.Asset{}, false, fmt.Errorf("%w: kind must be image or 3d", ErrBadRequest)
	}
	if in.SizeBytes < 0 {
		return store.Asset{}, false, fmt.Errorf("%w: sizeBytes must be >= 0", ErrBadRequest)
	}

	if existing, getErr := s.store.GetAssetByOwnerChecksumExt(ownerKey, checksum, ext); getErr == nil {
		if err := s.store.LinkAssetToFeature(in.FeatureID, existing.ID, in.Name, in.Description); err != nil {
			return store.Asset{}, false, err
		}
		return existing, false, nil
	}

	id, err := auth.NewRandomToken(12)
	if err != nil {
		return store.Asset{}, false, err
	}
	now := time.Now().UTC()
	isPublic := true
	if in.Visibility != nil {
		isPublic = *in.Visibility
	}
	asset := store.Asset{
		ID:        id,
		OwnerKey:  ownerKey,
		Checksum:  checksum,
		Ext:       ext,
		Kind:      in.Kind,
		MimeType:  in.MimeType,
		SizeBytes: in.SizeBytes,
		ObjectKey: ownerKey + "/" + checksum + "." + ext,
		IsPublic:  isPublic,
		CreatedAt: now,
		UpdatedAt: now,
	}
	s.store.SaveAsset(asset)
	if err := s.store.LinkAssetToFeature(in.FeatureID, asset.ID, in.Name, in.Description); err != nil {
		return store.Asset{}, false, err
	}
	return asset, true, nil
}

func (s *Service) SetAssetPublic(ownerKey, assetID string, isPublic bool) (store.Asset, error) {
	asset, err := s.store.GetAsset(assetID)
	if err != nil {
		return store.Asset{}, ErrAssetMiss
	}
	if asset.OwnerKey != ownerKey {
		return store.Asset{}, ErrForbidden
	}
	if err := s.store.SetAssetPublic(assetID, isPublic); err != nil {
		return store.Asset{}, err
	}
	asset.IsPublic = isPublic
	asset.UpdatedAt = time.Now().UTC()
	return asset, nil
}

func (s *Service) SignedUploadURL(ownerKey, assetID, contentType string) (string, error) {
	if s.assetSigner == nil {
		return "", ErrStorageNotConfigured
	}
	asset, err := s.store.GetAsset(assetID)
	if err != nil {
		return "", ErrAssetMiss
	}
	if asset.OwnerKey != ownerKey {
		return "", ErrForbidden
	}
	return "/v1/assets/" + asset.ID + "/upload", nil
}

func (s *Service) UploadAsset(ownerKey, assetID, contentType string, body io.Reader, size int64) error {
	if s.assetSigner == nil {
		return ErrStorageNotConfigured
	}
	asset, err := s.store.GetAsset(assetID)
	if err != nil {
		return ErrAssetMiss
	}
	if asset.OwnerKey != ownerKey {
		return ErrForbidden
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	return s.assetSigner.PutObject(context.Background(), asset.ObjectKey, contentType, body, size)
}

func (s *Service) SignedDownloadURL(requesterKey, assetID string) (string, error) {
	if s.assetSigner == nil {
		return "", ErrStorageNotConfigured
	}
	asset, err := s.store.GetAsset(assetID)
	if err != nil {
		return "", ErrAssetMiss
	}
	if asset.OwnerKey != requesterKey && !asset.IsPublic {
		return "", ErrForbidden
	}
	url, err := s.assetSigner.SignedGetObjectURL(context.Background(), asset.ObjectKey, s.config.ReadURLTTL)
	if err != nil {
		return "", err
	}
	return url, nil
}

func (s *Service) OpenAssetDownload(requesterKey, assetID string) (io.ReadCloser, string, int64, error) {
	if s.assetSigner == nil {
		return nil, "", 0, ErrStorageNotConfigured
	}
	asset, err := s.store.GetAsset(assetID)
	if err != nil {
		return nil, "", 0, ErrAssetMiss
	}
	if asset.OwnerKey != requesterKey && !asset.IsPublic {
		return nil, "", 0, ErrForbidden
	}
	return s.assetSigner.GetObject(context.Background(), asset.ObjectKey)
}
