package resource

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"net/url"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/movscript/movscript/internal/domain/media"
	domainresource "github.com/movscript/movscript/internal/domain/resource"
	"github.com/movscript/movscript/internal/infra/ai"
	"github.com/movscript/movscript/internal/infra/cache"
	"github.com/movscript/movscript/internal/infra/storage"
	"gorm.io/gorm"
)

var (
	ErrNotFound       = errors.New("resource not found")
	ErrFolderNotFound = errors.New("resource folder not found")
	ErrForbidden      = errors.New("resource access denied")
	ErrNoStorageKey   = errors.New("resource has no storage key")
	ErrDuplicateName  = errors.New("resource filename already exists")
	ErrResourceInUse  = errors.New("resource is still referenced")
)

type Service struct {
	repo     repository
	store    storage.Storage
	verifier ai.ImageVerificationClient
	cache    cache.Cache
}

const listCacheTTL = 60 * time.Second

func NewService(db *gorm.DB, store storage.Storage, verifier ai.ImageVerificationClient, cacheStore ...cache.Cache) *Service {
	var c cache.Cache
	if len(cacheStore) > 0 {
		c = cacheStore[0]
	}
	if c == nil {
		c = cache.NewNoop()
	}
	return &Service{repo: &gormRepository{db: db}, store: store, verifier: verifier, cache: c}
}

type ListInput struct {
	UserID   uint
	OrgID    *uint
	FolderID string
	Scope    string
	Type     string
	Query    string
	Page     int
	PageSize int
}

type Page struct {
	Total    int64                        `json:"total"`
	Items    []domainresource.RawResource `json:"items"`
	Page     int                          `json:"page"`
	PageSize int                          `json:"page_size"`
}

type UploadInput struct {
	UserID   uint
	OrgID    *uint
	FolderID string
	Filename string
	MimeType string
	Size     int64
	Data     []byte
}

type UpdateInput struct {
	UserID   uint
	OrgID    *uint
	ID       uint
	FolderID *uint
	Name     string
}

type VerifyImageInput struct {
	UserID uint
	OrgID  *uint
	ID     uint
}

func (s *Service) List(ctx context.Context, input ListInput) ([]domainresource.RawResource, *Page, error) {
	version, _ := s.cache.GetVersion(ctx, resourceListNamespace(input.UserID, input.OrgID))
	cacheKey := resourceListCacheKey(input, version)
	var cached cachedListResult
	if ok, err := s.cache.GetJSON(ctx, cacheKey, &cached); err == nil && ok {
		if cached.Page != nil {
			cached.Page.Items = cached.Resources
		}
		return cached.Resources, cached.Page, nil
	}
	resources, page, err := s.repo.List(ctx, input)
	if err == nil {
		_ = s.cache.SetJSON(ctx, cacheKey, cachedListResult{Resources: resources, Page: page}, listCacheTTL)
	}
	return resources, page, err
}

func (s *Service) Upload(ctx context.Context, input UploadInput) (domainresource.RawResource, error) {
	mimeType := normalizeUploadMimeType(input.MimeType, input.Filename)
	filename := strings.TrimSpace(input.Filename)
	data := input.Data
	size := input.Size
	if normalized, normalizedMime, changed, err := media.NormalizeVideoForBrowser(ctx, data, mimeType); err != nil {
		fmt.Printf("[resource] video normalization skipped for %q: %v\n", input.Filename, err)
	} else if changed {
		data = normalized
		mimeType = normalizedMime
		filename = media.MP4Name(filename)
		size = int64(len(data))
	}
	if err := s.ensureUniqueResourceName(ctx, input.UserID, input.OrgID, filename, 0); err != nil {
		return domainresource.RawResource{}, err
	}
	blob, err := s.ensureBlobForData(ctx, data, mimeType)
	if err != nil {
		return domainresource.RawResource{}, err
	}

	var r domainresource.RawResource
	if err := s.repo.Transaction(ctx, func(repo repository) error {
		folderID, err := repo.UploadFolderID(ctx, input.UserID, input.OrgID, input.FolderID)
		if err != nil {
			return err
		}
		if err := s.ensureUniqueResourceNameWithRepo(ctx, repo, input.UserID, input.OrgID, filename, 0); err != nil {
			return err
		}
		r = domainresource.NewUploadedResource(domainresource.NewUploadedResourceSpec{
			OwnerID:        input.UserID,
			OrgID:          input.OrgID,
			FolderID:       folderID,
			Name:           filename,
			MimeType:       mimeType,
			Size:           size,
			StorageBackend: s.store.Backend(),
		})
		r.BlobID = &blob.ID
		r.FilePath = "stored:" + blob.StorageKey
		r.StorageKey = blob.StorageKey
		r.StorageBackend = blob.StorageBackend
		if err := repo.CreateResource(ctx, &r); err != nil {
			return err
		}
		return repo.IncrementBlobRef(ctx, blob.ID)
	}); err != nil {
		return domainresource.RawResource{}, err
	}
	s.bumpListVersion(ctx, input.UserID, input.OrgID)
	return r, nil
}

func (s *Service) GetVisible(ctx context.Context, id uint, userID uint, orgID *uint) (domainresource.RawResource, error) {
	return s.repo.GetVisible(ctx, id, userID, orgID)
}

func (s *Service) AdoptToTeam(ctx context.Context, id uint, userID uint, orgID *uint) (domainresource.RawResource, error) {
	resource, err := s.repo.AdoptOwnedPersonalResourceToOrg(ctx, id, userID, orgID)
	if err != nil {
		return resource, err
	}
	s.bumpListVersion(ctx, userID, nil)
	s.bumpListVersion(ctx, userID, orgID)
	return resource, nil
}

func (s *Service) Delete(ctx context.Context, id uint, userID uint, orgID *uint) error {
	if err := s.repo.Transaction(ctx, func(repo repository) error {
		r, err := repo.GetOwned(ctx, id, userID, orgID)
		if err != nil {
			return err
		}
		refs, err := repo.ResourceReferenceCount(ctx, r.ID)
		if err != nil {
			return err
		}
		if refs > 0 {
			return ErrResourceInUse
		}
		if err := repo.DeleteResourceRecord(ctx, &r); err != nil {
			return err
		}
		if r.BlobID != nil {
			return repo.DecrementBlobRef(ctx, *r.BlobID)
		}
		return nil
	}); err != nil {
		return err
	}
	s.bumpListVersion(ctx, userID, orgID)
	return nil
}

func (s *Service) Update(ctx context.Context, input UpdateInput) (domainresource.RawResource, error) {
	r, err := s.repo.GetOwned(ctx, input.ID, input.UserID, input.OrgID)
	if err != nil {
		return r, err
	}
	var updates domainresource.UpdateSpec
	if input.FolderID != nil {
		if *input.FolderID == 0 {
			updates.ClearFolder = true
		} else {
			folderID, err := s.repo.UploadFolderID(ctx, input.UserID, input.OrgID, strconv.FormatUint(uint64(*input.FolderID), 10))
			if err != nil {
				return r, err
			}
			if folderID == nil {
				return r, ErrFolderNotFound
			}
			updates.FolderID = folderID
		}
	}
	if name := strings.TrimSpace(input.Name); name != "" {
		if err := s.ensureUniqueResourceName(ctx, input.UserID, input.OrgID, name, r.ID); err != nil {
			return r, err
		}
		updates.Name = &name
	}
	if !updates.Empty() {
		if err := s.repo.UpdateResourceRecord(ctx, &r, updates); err != nil {
			return r, err
		}
	}
	if err := s.repo.ReloadResource(ctx, &r); err != nil {
		return r, err
	}
	s.bumpListVersion(ctx, input.UserID, input.OrgID)
	return r, nil
}

func (s *Service) ensureUniqueResourceName(ctx context.Context, userID uint, orgID *uint, name string, excludeID uint) error {
	return s.ensureUniqueResourceNameWithRepo(ctx, s.repo, userID, orgID, name, excludeID)
}

func (s *Service) ensureUniqueResourceNameWithRepo(ctx context.Context, repo repository, userID uint, orgID *uint, name string, excludeID uint) error {
	if strings.TrimSpace(name) == "" {
		return nil
	}
	exists, err := repo.ResourceNameExists(ctx, resourceNameScope{
		UserID:    userID,
		OrgID:     orgID,
		Name:      name,
		ExcludeID: excludeID,
	})
	if err != nil {
		return err
	}
	if exists {
		return ErrDuplicateName
	}
	return nil
}

func (s *Service) ensureBlobForData(ctx context.Context, data []byte, mimeType string) (resourceBlob, error) {
	return s.ensureBlobForDataWithRepo(ctx, s.repo, data, mimeType)
}

func (s *Service) ensureBlobForDataWithRepo(ctx context.Context, repo repository, data []byte, mimeType string) (resourceBlob, error) {
	hash := sha256Hex(data)
	if existing, ok, err := repo.FindBlobByHash(ctx, hash); err != nil {
		return resourceBlob{}, err
	} else if ok {
		return existing, nil
	}
	key := domainresource.GenerateBlobStorageKey(hash)
	if err := s.store.Put(ctx, key, bytes.NewReader(data), int64(len(data)), mimeType); err != nil {
		return resourceBlob{}, err
	}
	blob := resourceBlob{
		Hash:           hash,
		StorageBackend: s.store.Backend(),
		StorageKey:     key,
		Size:           int64(len(data)),
		MimeType:       mimeType,
	}
	if err := repo.CreateBlob(ctx, &blob); err != nil {
		if existing, ok, findErr := repo.FindBlobByHash(ctx, hash); findErr != nil {
			return resourceBlob{}, findErr
		} else if ok {
			return existing, nil
		}
		return resourceBlob{}, err
	}
	return blob, nil
}

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

func (s *Service) VerifyImage(ctx context.Context, input VerifyImageInput) (domainresource.RawResource, error) {
	r, err := s.repo.GetOwned(ctx, input.ID, input.UserID, input.OrgID)
	if err != nil {
		return r, err
	}
	if r.Type != "image" {
		return r, errors.New("resource is not an image")
	}
	if s.verifier == nil {
		return r, errors.New("image verifier is not configured")
	}
	ref, err := s.buildVerificationRef(ctx, r)
	if err != nil {
		return r, err
	}
	result, err := s.verifier.VerifyImage(ctx, ai.ImageVerificationRequest{
		ImageURL: ref,
		MimeType: r.MimeType,
	})
	if err != nil {
		status := string(ai.ImageVerificationRejected)
		errText := err.Error()
		_ = s.repo.UpdateResourceRecord(ctx, &r, domainresource.UpdateSpec{
			VerificationStatus:   &status,
			VerificationRef:      &ref,
			VerificationProvider: ptrString("seeaance"),
			VerificationError:    &errText,
		})
		return r, err
	}
	status := string(result.Status)
	if status == "" {
		status = string(ai.ImageVerificationPending)
	}
	verifiedAt := result.CheckedAt
	if verifiedAt.IsZero() {
		verifiedAt = time.Now().UTC()
	}
	updates := domainresource.UpdateSpec{
		VerificationStatus:   &status,
		VerificationRef:      &result.Ref,
		VerifiedAt:           &verifiedAt,
		VerificationProvider: &result.Provider,
		VerificationError:    stringPtr(result.Message),
	}
	if err := s.repo.UpdateResourceRecord(ctx, &r, updates); err != nil {
		return r, err
	}
	if err := s.repo.ReloadResource(ctx, &r); err != nil {
		return r, err
	}
	return r, nil
}

func (s *Service) buildVerificationRef(ctx context.Context, r domainresource.RawResource) (string, error) {
	if r.StorageKey != "" && s.store != nil {
		if url, err := s.store.DirectURL(ctx, r.StorageKey); err == nil && url != "" {
			return url, nil
		}
	}
	if r.URL != "" {
		return r.URL, nil
	}
	if r.DirectURL != "" {
		return r.DirectURL, nil
	}
	if r.FilePath != "" {
		return "file://" + r.FilePath, nil
	}
	return "", errors.New("resource has no verifiable reference")
}

type cachedListResult struct {
	Resources []domainresource.RawResource `json:"resources"`
	Page      *Page                        `json:"page,omitempty"`
}

func (s *Service) bumpListVersion(ctx context.Context, userID uint, orgID *uint) {
	_, _ = s.cache.BumpVersion(ctx, resourceListNamespace(userID, orgID))
}

func resourceListNamespace(userID uint, orgID *uint) string {
	return fmt.Sprintf("resources:user:%d:org:%s", userID, orgIDCachePart(orgID))
}

func resourceListCacheKey(input ListInput, version int64) string {
	values := url.Values{}
	values.Set("folder_id", strings.TrimSpace(input.FolderID))
	values.Set("scope", strings.TrimSpace(input.Scope))
	values.Set("type", strings.TrimSpace(input.Type))
	values.Set("q", strings.TrimSpace(input.Query))
	values.Set("page", strconv.Itoa(input.Page))
	values.Set("page_size", strconv.Itoa(input.PageSize))
	return fmt.Sprintf("%s:v%d:%s", resourceListNamespace(input.UserID, input.OrgID), version, values.Encode())
}

func orgIDCachePart(orgID *uint) string {
	if orgID == nil {
		return "none"
	}
	return strconv.FormatUint(uint64(*orgID), 10)
}

func ptrString(value string) *string {
	return &value
}

func stringPtr(value string) *string {
	return &value
}

func MimeToType(mime, filename string) string {
	return domainresource.MimeToType(mime, filename)
}

func normalizeUploadMimeType(mimeType, filename string) string {
	base := strings.TrimSpace(strings.Split(mimeType, ";")[0])
	if base != "" && base != "application/octet-stream" {
		return mimeType
	}
	switch strings.ToLower(filepath.Ext(filename)) {
	case ".heic":
		return "image/heic"
	case ".heif":
		return "image/heif"
	default:
		return mimeType
	}
}

func GenerateStorageKey(resourceID uint, filename string) string {
	return domainresource.GenerateStorageKey(resourceID, filename)
}
