package drive

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

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/api/paginate"
	"github.com/ultisuite/ulti-backend/internal/api/query"
	"github.com/ultisuite/ulti-backend/internal/auth"
	"github.com/ultisuite/ulti-backend/internal/automation"
	"github.com/ultisuite/ulti-backend/internal/filescan"
	"github.com/ultisuite/ulti-backend/internal/mail/rules"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	"github.com/ultisuite/ulti-backend/internal/publicshare"
	"github.com/ultisuite/ulti-backend/internal/realtime"
)

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

type Service struct {
	nc               *nextcloud.Client
	hub              *realtime.Hub
	db               *pgxpool.Pool
	automation       driveAutomation
	scanner          *filescan.Scanner
	maxUploadBytes   int64
	quotaReserveByte int64
}

type driveAutomation interface {
	OnDriveEvent(ctx context.Context, externalUserID string, trigger rules.TriggerType, payload automation.DrivePayload)
}

func NewService(nc *nextcloud.Client, hub *realtime.Hub, db *pgxpool.Pool) *Service {
	return &Service{
		nc:               nc,
		hub:              hub,
		db:               db,
		maxUploadBytes:   envInt64("ULTID_DRIVE_MAX_UPLOAD_BYTES", 0),
		quotaReserveByte: envInt64("ULTID_DRIVE_QUOTA_RESERVED_BYTES", 0),
	}
}

func (s *Service) EnsureNextcloudUser(ctx context.Context, claims *auth.Claims) (string, error) {
	if s.nc == nil {
		return "", errors.New("nextcloud unavailable")
	}
	return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
}

func (s *Service) notifyFileChanged(platformUserID, path string) {
	if s.hub == nil || platformUserID == "" {
		return
	}
	s.hub.Broadcast(platformUserID, realtime.NewDriveFileChangedEvent(path))
}

// PublishFileChanged notifies connected clients that a drive path changed.
func (s *Service) PublishFileChanged(platformUserID, filePath string) {
	s.notifyFileChanged(platformUserID, nextcloud.NormalizeClientPath(filePath))
}

func (s *Service) notifyShareUpdated(platformUserID, path string) {
	if s.hub == nil || platformUserID == "" {
		return
	}
	s.hub.Broadcast(platformUserID, realtime.NewDriveShareUpdatedEvent(path))
}

type FilesList struct {
	Files      []nextcloud.FileInfo `json:"files"`
	Pagination query.PaginationMeta `json:"pagination,omitempty"`
}

func (s *Service) ListFilterCorpus(ctx context.Context, userID, path string) (FilesList, error) {
	if path == "" {
		path = "/"
	}
	files, err := s.nc.ListFilesRecursive(ctx, userID, path, 0)
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}
	total := int64(len(files))
	return FilesList{
		Files:      files,
		Pagination: query.ListParams{Page: 1, PageSize: len(files)}.Meta(&total),
	}, nil
}

func (s *Service) ListFiles(ctx context.Context, userID, path string, params query.ListParams) (FilesList, error) {
	if path == "" {
		path = "/"
	}
	files, err := s.nc.ListFiles(ctx, userID, path)
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}
	filtered := filterFiles(files, params.Q)
	page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
	return FilesList{
		Files:      page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) ListTrash(ctx context.Context, userID string, params query.ListParams) (FilesList, error) {
	files, err := s.nc.ListTrash(ctx, userID)
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}
	filtered := filterFiles(files, params.Q)
	page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
	return FilesList{
		Files:      page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) ListRecent(ctx context.Context, userID string, params query.ListParams) (FilesList, error) {
	files, err := s.nc.ListRecent(ctx, userID, params.Limit())
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}
	filtered := filterFiles(files, params.Q)
	page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
	return FilesList{
		Files:      page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) ListSharedWithMe(ctx context.Context, userID string, params query.ListParams) (FilesList, error) {
	files, err := s.nc.ListSharedWithMe(ctx, userID)
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}
	filtered := filterFiles(files, params.Q)
	page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
	return FilesList{
		Files:      page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) ListStarred(ctx context.Context, userID, basePath string, params query.ListParams) (FilesList, error) {
	if basePath == "" {
		basePath = "/"
	}
	limit := params.Limit()
	if limit <= 0 {
		limit = 50
	}
	collect := limit + params.Offset()
	if collect <= 0 || collect > 500 {
		collect = 500
	}
	starred, err := s.nc.ListFavorites(ctx, userID, basePath, int(collect))
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}
	filtered := filterFiles(starred, params.Q)
	page, total := paginate.Slice(filtered, params.Offset(), limit)
	return FilesList{
		Files:      page,
		Pagination: params.Meta(&total),
	}, nil
}

func (s *Service) Upload(ctx context.Context, userID, path string, body io.Reader, contentType string, contentLength int64) error {
	if err := s.ensureQuota(ctx, userID, contentLength); err != nil {
		return err
	}
	reader := body
	if s.scanner != nil {
		data, _, err := s.scanner.ScanReader(ctx, path, body, contentLength)
		if err != nil {
			if errors.Is(err, filescan.ErrMalicious) {
				return ErrMalware
			}
			return err
		}
		reader = bytes.NewReader(data)
	}
	return mapDriveError(s.nc.Upload(ctx, userID, path, reader, contentType))
}

func (s *Service) UploadChunk(ctx context.Context, userID, uploadID, targetPath string, chunk ChunkUpload, body io.Reader, contentType string) error {
	if err := mapDriveError(s.nc.UploadChunk(ctx, userID, uploadID, chunkName(chunk.Index), body, contentType)); err != nil {
		return err
	}
	if !chunk.Complete {
		return nil
	}

	if err := s.ensureQuota(ctx, userID, chunk.TotalSize); err != nil {
		_ = s.nc.AbortChunkUpload(ctx, userID, uploadID)
		return err
	}
	if err := mapDriveError(s.nc.AssembleChunks(ctx, userID, uploadID, targetPath, chunk.TotalSize)); err != nil {
		return err
	}
	if s.scanner != nil {
		if err := s.scanAssembledUpload(ctx, userID, targetPath); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) scanAssembledUpload(ctx context.Context, userID, targetPath string) error {
	body, _, err := s.nc.Download(ctx, userID, targetPath)
	if err != nil {
		return mapDriveError(err)
	}
	defer body.Close()

	data, scanResult, err := s.scanner.ScanReader(ctx, targetPath, body, -1)
	if err != nil {
		if errors.Is(err, filescan.ErrMalicious) {
			_ = s.nc.Delete(ctx, userID, targetPath)
			return ErrMalware
		}
		return err
	}
	_ = data
	_ = scanResult
	return nil
}

func (s *Service) Download(ctx context.Context, userID, path string) (io.ReadCloser, string, error) {
	body, contentType, err := s.nc.Download(ctx, userID, path)
	if err != nil {
		return nil, "", mapDriveError(err)
	}
	return body, contentType, nil
}

func (s *Service) Preview(ctx context.Context, userID, filePath string, width, height int) (io.ReadCloser, string, error) {
	filePath = nextcloud.NormalizeClientFilePath(userID, filePath)
	fileID, err := s.nc.FileID(ctx, userID, filePath)
	if err != nil {
		return nil, "", mapDriveError(err)
	}
	body, contentType, err := s.nc.Preview(ctx, userID, fileID, width, height)
	if err != nil {
		return nil, "", mapDriveError(err)
	}
	return body, contentType, nil
}

func (s *Service) Delete(ctx context.Context, userID, path string) error {
	return mapDriveError(s.nc.Delete(ctx, userID, path))
}

func (s *Service) CreateFolder(ctx context.Context, userID, path string) error {
	return mapDriveError(s.nc.CreateFolder(ctx, userID, path))
}

func (s *Service) Move(ctx context.Context, userID, source, destination string) error {
	source = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(source))
	destination = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(destination))
	return mapDriveError(s.nc.Move(ctx, userID, source, destination))
}

func (s *Service) Copy(ctx context.Context, userID, source, destination string) error {
	source = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(source))
	destination = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(destination))
	return mapDriveError(s.nc.Copy(ctx, userID, source, destination))
}

func (s *Service) Rename(ctx context.Context, userID, filePath, newName string) error {
	if strings.Contains(newName, "/") {
		return ErrInvalid
	}
	filePath = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(filePath))
	dir := path.Dir("/" + strings.TrimPrefix(filePath, "/"))
	destination := path.Join(dir, newName)
	return mapDriveError(s.nc.Move(ctx, userID, filePath, destination))
}

func (s *Service) CreateShare(ctx context.Context, userID, filePath string, req createShareRequest, permissions int) (*nextcloud.ShareInfo, error) {
	filePath = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(filePath))
	opts, err := s.buildCreateShareOptions(ctx, req, permissions)
	if err != nil {
		return nil, err
	}
	share, err := s.nc.CreateShare(ctx, userID, filePath, opts)
	if err != nil {
		return nil, mapDriveError(err)
	}
	s.rewriteShareURL(share)
	if shouldSendShareEmail(opts, req) {
		if sendErr := s.nc.SendShareEmail(ctx, userID, share.ID, ""); sendErr != nil {
			return share, nil
		}
	}
	return share, nil
}

func (s *Service) UserExists(ctx context.Context, email string) (bool, error) {
	userID := nextcloud.UserIDFromClaims(strings.TrimSpace(email), "")
	if userID == "" {
		return false, nil
	}
	return s.nc.UserExists(ctx, userID)
}

func (s *Service) SendShareEmail(ctx context.Context, userID, shareID, password string) error {
	return mapDriveError(s.nc.SendShareEmail(ctx, userID, shareID, password))
}

func shouldSendShareEmail(opts nextcloud.CreateShareOptions, req createShareRequest) bool {
	// Fallback when NC did not send the invitation email on create (common for mail shares).
	if opts.ShareType != 4 {
		return false
	}
	return req.SendMail == nil || *req.SendMail
}

func (s *Service) buildCreateShareOptions(ctx context.Context, req createShareRequest, permissions int) (nextcloud.CreateShareOptions, error) {
	opts := nextcloud.CreateShareOptions{Permissions: permissions}
	mode := strings.TrimSpace(strings.ToLower(req.Mode))
	sendMail := req.SendMail == nil || *req.SendMail

	switch mode {
	case "internal":
		opts.ShareType = 3
		opts.Label = "internal"
		opts.AccessMode = "internal"
	case "contact":
		email := strings.TrimSpace(strings.ToLower(req.ShareWith))
		if email == "" {
			return opts, ErrInvalid
		}
		registered, err := s.UserExists(ctx, email)
		if err != nil {
			return opts, err
		}
		opts.Note = strings.TrimSpace(req.Note)
		opts.SendMail = sendMail
		if registered {
			opts.ShareType = 0
			opts.ShareWith = nextcloud.UserIDFromClaims(email, "")
			opts.AccessMode = "user"
		} else {
			opts.ShareType = 4
			opts.ShareWith = email
			opts.AccessMode = "email"
		}
	case "public":
		opts.ShareType = 3
		opts.AccessMode = "public"
	default:
		opts.ShareType = req.ShareType
		if opts.ShareType == 0 {
			opts.ShareType = 3
		}
		if opts.ShareType == 3 {
			opts.AccessMode = "public"
		}
	}
	return opts, nil
}

func (s *Service) GetQuota(ctx context.Context, userID string) (nextcloud.UserQuota, error) {
	quota, err := s.nc.GetQuota(ctx, userID)
	if err != nil {
		return nextcloud.UserQuota{}, mapDriveError(err)
	}
	return quota, nil
}

func (s *Service) ListShares(ctx context.Context, userID, filePath string) ([]nextcloud.ShareInfo, error) {
	filePath = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(filePath))
	shares, err := s.nc.ListShares(ctx, userID, filePath)
	if err != nil {
		return nil, mapDriveError(err)
	}
	s.rewriteShareURLs(shares)
	return shares, nil
}

func (s *Service) GetPublicShare(ctx context.Context, token, path, password string) (*nextcloud.PublicShareView, error) {
	view, err := s.nc.GetPublicShare(ctx, token, path, password)
	if err != nil {
		return nil, mapPublicShareError(err)
	}
	s.recordPublicShareAccess(ctx, token)
	return view, nil
}

func (s *Service) DownloadPublicShare(ctx context.Context, token, filePath, password string) (io.ReadCloser, string, error) {
	body, contentType, err := s.nc.DownloadPublicShare(ctx, token, filePath, password)
	if err != nil {
		return nil, "", mapPublicShareError(err)
	}
	s.recordPublicShareAccess(ctx, token)
	return body, contentType, nil
}

func (s *Service) PreviewPublicShare(ctx context.Context, token, filePath, password string, width, height int) (io.ReadCloser, string, error) {
	body, contentType, err := s.nc.PreviewPublicShare(ctx, token, filePath, password, width, height)
	if err != nil {
		return nil, "", mapPublicShareError(err)
	}
	s.recordPublicShareAccess(ctx, token)
	return body, contentType, nil
}

func (s *Service) recordPublicShareAccess(ctx context.Context, token string) {
	publicshare.RecordAccess(ctx, s.db, token)
}

func (s *Service) rewriteShareURL(share *nextcloud.ShareInfo) {
	if share == nil || s.nc == nil {
		return
	}
	if share.ShareType == 3 && strings.TrimSpace(share.Token) != "" {
		if u := s.nc.PublicShareURL(share.Token); u != "" {
			share.URL = u
		}
	}
}

func (s *Service) rewriteShareURLs(shares []nextcloud.ShareInfo) {
	for i := range shares {
		s.rewriteShareURL(&shares[i])
	}
}

func mapPublicShareError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, nextcloud.ErrPublicSharePasswordRequired) {
		return ErrForbidden
	}
	if errors.Is(err, nextcloud.ErrInvalidPublicShare) {
		return ErrInvalid
	}
	return mapDriveError(err)
}

func (s *Service) UpdateShare(ctx context.Context, userID, shareID string, permissions int, expireDate, password string) (*nextcloud.ShareInfo, error) {
	share, err := s.nc.UpdateShare(ctx, userID, shareID, permissions, expireDate, password)
	if err != nil {
		return nil, mapDriveError(err)
	}
	return share, nil
}

func (s *Service) DeleteShare(ctx context.Context, userID, shareID string) error {
	return mapDriveError(s.nc.DeleteShare(ctx, userID, shareID))
}

func (s *Service) RestoreTrash(ctx context.Context, userID, trashName string) error {
	return mapDriveError(s.nc.RestoreFromTrash(ctx, userID, trashName))
}

func (s *Service) DeleteTrash(ctx context.Context, userID, trashName string) error {
	return mapDriveError(s.nc.DeleteFromTrash(ctx, userID, trashName))
}

func (s *Service) EmptyTrash(ctx context.Context, userID string) error {
	return mapDriveError(s.nc.EmptyTrash(ctx, userID))
}

func (s *Service) SetFavorite(ctx context.Context, userID, filePath string, favorite bool) error {
	filePath = nextcloud.NormalizeClientFilePath(userID, nextcloud.NormalizeClientPath(filePath))
	return mapDriveError(s.nc.SetFavorite(ctx, userID, filePath, favorite))
}

func (s *Service) Search(ctx context.Context, userID string, opts SearchOptions, params query.ListParams) (FilesList, error) {
	q := strings.TrimSpace(params.Q)
	if q == "" {
		return FilesList{
			Files:      []nextcloud.FileInfo{},
			Pagination: params.Meta(ptrInt64(0)),
		}, nil
	}

	limit := params.Limit()
	if limit <= 0 {
		if opts.Suggest {
			limit = 8
		} else {
			limit = 100
		}
	}

	scope := nextcloud.SearchScope(opts.Scope)
	if scope == "" {
		scope = nextcloud.SearchScopeAll
	}

	files, err := s.nc.SearchFiles(ctx, userID, nextcloud.SearchOptions{
		Query:    q,
		Scope:    scope,
		BasePath: opts.BasePath,
		Suggest:  opts.Suggest,
		Limit:    limit + params.Offset(),
	})
	if err != nil {
		return FilesList{}, mapDriveError(err)
	}

	page, total := paginate.Slice(files, params.Offset(), limit)
	return FilesList{
		Files:      page,
		Pagination: params.Meta(&total),
	}, nil
}

type SearchOptions struct {
	Scope    string
	BasePath string
	Suggest  bool
}

func ptrInt64(v int64) *int64 {
	return &v
}

type NewFileKind string

const (
	NewFileDocument      NewFileKind = "document"
	NewFileSpreadsheet   NewFileKind = "spreadsheet"
	NewFilePresentation  NewFileKind = "presentation"
)

func (s *Service) CreateNewFile(ctx context.Context, userID, parentPath, name string, kind NewFileKind) (string, error) {
	if strings.TrimSpace(name) == "" {
		return "", ErrInvalid
	}
	content, contentType := blankOfficeFile(kind)
	if content == nil {
		return "", ErrInvalid
	}
	target := path.Join(strings.TrimSuffix(parentPath, "/"), name)
	if err := mapDriveError(s.nc.Upload(ctx, userID, target, bytes.NewReader(content), contentType)); err != nil {
		return "", err
	}
	return target, nil
}

func filterFiles(files []nextcloud.FileInfo, q string) []nextcloud.FileInfo {
	q = strings.ToLower(strings.TrimSpace(q))
	if q == "" {
		return files
	}
	out := make([]nextcloud.FileInfo, 0, len(files))
	for _, f := range files {
		if strings.Contains(strings.ToLower(f.Name), q) ||
			strings.Contains(strings.ToLower(f.Path), q) {
			out = append(out, f)
		}
	}
	return out
}

type ChunkUpload struct {
	Index     int
	Total     int
	TotalSize int64
	Complete  bool
}

func chunkName(index int) string {
	return strconv.FormatInt(int64(index), 10)
}

func (s *Service) ensureQuota(ctx context.Context, userID string, incomingBytes int64) error {
	if s.maxUploadBytes > 0 && incomingBytes > s.maxUploadBytes {
		return ErrQuotaExceeded
	}
	quota, err := s.nc.GetQuota(ctx, userID)
	if err != nil {
		return mapDriveError(err)
	}
	if !quotaAllowsUpload(quota.Free, incomingBytes, s.quotaReserveByte) {
		return ErrQuotaExceeded
	}
	return nil
}

// quotaAllowsUpload mirrors Nextcloud quota semantics: negative free means unknown or unlimited.
func quotaAllowsUpload(free, incomingBytes, reserve int64) bool {
	if incomingBytes <= 0 {
		return true
	}
	if free < 0 {
		return true
	}
	if free == 0 {
		return false
	}
	return incomingBytes+reserve <= free
}

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

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
}

func parseModTime(value string) time.Time {
	if value == "" {
		return time.Time{}
	}
	if t, err := time.Parse(time.RFC3339, value); err == nil {
		return t
	}
	if t, err := time.Parse(time.RFC1123, value); err == nil {
		return t
	}
	return time.Time{}
}
