package photos

import (
	"context"
	"errors"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"

	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	photospkg "github.com/ultisuite/ulti-backend/internal/photos"
)

var (
	ErrNotFound      = errors.New("not found")
	ErrConflict      = errors.New("conflict")
	ErrInvalid       = errors.New("invalid request")
	ErrForbidden     = errors.New("forbidden")
	ErrQuotaExceeded = errors.New("quota exceeded")
)

type quotaProvider interface {
	GetQuota(ctx context.Context, userID string) (nextcloud.UserQuota, error)
}

type Service struct {
	client           *photospkg.Client
	quota            quotaProvider
	maxUploadBytes   int64
	quotaReserveByte int64
}

func NewService(client *photospkg.Client, quota quotaProvider) *Service {
	return &Service{
		client:           client,
		quota:            quota,
		maxUploadBytes:   envInt64("ULTID_PHOTOS_MAX_UPLOAD_BYTES", 0),
		quotaReserveByte: envInt64("ULTID_PHOTOS_QUOTA_RESERVED_BYTES", 0),
	}
}

type AssetsList struct {
	Assets     []photospkg.Asset    `json:"assets"`
	Page       int                  `json:"page"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListAssets(ctx context.Context, apiKey string, params query.ListParams) (AssetsList, error) {
	assets, err := s.client.GetAllAssets(ctx, apiKey)
	if err != nil {
		return AssetsList{}, mapPhotosError(err)
	}
	page, total := applyAssetList(assets, params)
	return AssetsList{
		Assets:     page,
		Page:       params.Page,
		Pagination: params.Meta(&total),
	}, nil
}

type AlbumsList struct {
	Albums     []photospkg.Album    `json:"albums"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListAlbums(ctx context.Context, apiKey string, params query.ListParams) (AlbumsList, error) {
	albums, err := s.client.GetAlbums(ctx, apiKey)
	if err != nil {
		return AlbumsList{}, mapPhotosError(err)
	}
	page, total := applyAlbumList(albums, params)
	return AlbumsList{
		Albums:     page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) GetAlbum(ctx context.Context, apiKey, albumID string) (photospkg.Album, error) {
	album, err := s.client.GetAlbum(ctx, apiKey, albumID)
	if err != nil {
		return photospkg.Album{}, mapPhotosError(err)
	}
	return album, nil
}

func (s *Service) CreateAlbum(ctx context.Context, apiKey string, req *createAlbumRequest) (photospkg.Album, error) {
	album, err := s.client.CreateAlbum(ctx, apiKey, strings.TrimSpace(req.Name), strings.TrimSpace(req.Description), req.AssetIDs)
	if err != nil {
		return photospkg.Album{}, mapPhotosError(err)
	}
	return album, nil
}

func (s *Service) UpdateAlbum(ctx context.Context, apiKey, albumID string, req *updateAlbumRequest) (photospkg.Album, error) {
	var name, description *string
	if req.Name != nil {
		trimmed := strings.TrimSpace(*req.Name)
		name = &trimmed
	}
	if req.Description != nil {
		trimmed := strings.TrimSpace(*req.Description)
		description = &trimmed
	}
	album, err := s.client.UpdateAlbum(ctx, apiKey, albumID, name, description)
	if err != nil {
		return photospkg.Album{}, mapPhotosError(err)
	}
	return album, nil
}

func (s *Service) DeleteAlbum(ctx context.Context, apiKey, albumID string) error {
	return mapPhotosError(s.client.DeleteAlbum(ctx, apiKey, albumID))
}

type AlbumAssetsResult struct {
	Results []photospkg.BulkAssetResult `json:"results"`
}

func (s *Service) AddAlbumAssets(ctx context.Context, apiKey, albumID string, assetIDs []string) (AlbumAssetsResult, error) {
	results, err := s.client.AddAlbumAssets(ctx, apiKey, albumID, assetIDs)
	if err != nil {
		return AlbumAssetsResult{}, mapPhotosError(err)
	}
	return AlbumAssetsResult{Results: results}, nil
}

func (s *Service) RemoveAlbumAssets(ctx context.Context, apiKey, albumID string, assetIDs []string) (AlbumAssetsResult, error) {
	results, err := s.client.RemoveAlbumAssets(ctx, apiKey, albumID, assetIDs)
	if err != nil {
		return AlbumAssetsResult{}, mapPhotosError(err)
	}
	return AlbumAssetsResult{Results: results}, nil
}

func mapPhotosError(err error) error {
	if err == nil {
		return nil
	}
	var statusErr *photospkg.HTTPStatusError
	if !errors.As(err, &statusErr) {
		return err
	}
	switch statusErr.StatusCode {
	case http.StatusNotFound:
		return ErrNotFound
	case http.StatusConflict:
		return ErrConflict
	case http.StatusForbidden, http.StatusUnauthorized:
		return ErrForbidden
	case http.StatusInsufficientStorage, http.StatusRequestEntityTooLarge:
		return ErrQuotaExceeded
	case http.StatusBadRequest, http.StatusUnprocessableEntity:
		return ErrInvalid
	default:
		return err
	}
}

func (s *Service) UploadAsset(ctx context.Context, apiKey string, body io.Reader, contentType string) (string, error) {
	if err := s.ensureQuota(ctx, apiKey); err != nil {
		return "", err
	}
	id, err := s.client.UploadAsset(ctx, apiKey, body, contentType)
	return id, mapPhotosError(err)
}

func (s *Service) GetAssetThumbnail(ctx context.Context, apiKey, assetID string) (io.ReadCloser, string, error) {
	body, contentType, err := s.client.GetAssetThumbnail(ctx, apiKey, assetID)
	return body, contentType, mapPhotosError(err)
}

func (s *Service) DeleteAsset(ctx context.Context, apiKey, assetID string) error {
	return mapPhotosError(s.client.DeleteAsset(ctx, apiKey, assetID))
}

func (s *Service) ensureQuota(ctx context.Context, userID string) error {
	if s.quota == nil {
		return nil
	}

	incomingBytes := requestContentLength(ctx)
	if incomingBytes > 0 && s.maxUploadBytes > 0 && incomingBytes > s.maxUploadBytes {
		return ErrQuotaExceeded
	}
	if incomingBytes <= 0 {
		return nil
	}

	quota, err := s.quota.GetQuota(ctx, userID)
	if err != nil {
		return err
	}
	if quota.Free <= 0 || incomingBytes+s.quotaReserveByte > quota.Free {
		return ErrQuotaExceeded
	}
	return nil
}

func requestContentLength(ctx context.Context) int64 {
	v := ctx.Value(contentLengthContextKey{})
	n, ok := v.(int64)
	if !ok {
		return 0
	}
	return n
}

func envInt64(key string, fallback int64) int64 {
	raw := strings.TrimSpace(os.Getenv(key))
	if raw == "" {
		return fallback
	}
	n, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return fallback
	}
	return n
}
