package explorer

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"time"

	"github.com/cloudreve/Cloudreve/v4/application/dependency"
	"github.com/cloudreve/Cloudreve/v4/ent"
	"github.com/cloudreve/Cloudreve/v4/inventory"
	"github.com/cloudreve/Cloudreve/v4/inventory/types"
	"github.com/cloudreve/Cloudreve/v4/pkg/auth"
	"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
	"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
	"github.com/cloudreve/Cloudreve/v4/pkg/queue"
	"github.com/cloudreve/Cloudreve/v4/pkg/util"
	"github.com/cloudreve/Cloudreve/v4/service/user"
	"github.com/gin-gonic/gin"
	"github.com/gofrs/uuid"
	"github.com/samber/lo"
)

type PutRelativeResponse struct {
	Name string
	Url  string
}

type DirectLinkResponse struct {
	Link    string `json:"link"`
	FileUrl string `json:"file_url"`
}

func BuildDirectLinkResponse(links []manager.DirectLink) []DirectLinkResponse {
	if len(links) == 0 {
		return nil
	}

	var res []DirectLinkResponse
	for _, link := range links {
		res = append(res, DirectLinkResponse{
			Link:    link.Url,
			FileUrl: link.File.Uri(false).String(),
		})
	}
	return res
}

const PathMyRedacted = "redacted"

type TaskResponse struct {
	CreatedAt    time.Time      `json:"created_at,"`
	UpdatedAt    time.Time      `json:"updated_at"`
	ID           string         `json:"id"`
	Status       string         `json:"status"`
	Type         string         `json:"type"`
	Node         *user.Node     `json:"node,omitempty"`
	Summary      *queue.Summary `json:"summary,omitempty"`
	Error        string         `json:"error,omitempty"`
	ErrorHistory []string       `json:"error_history,omitempty"`
	Duration     int64          `json:"duration,omitempty"`
	ResumeTime   int64          `json:"resume_time,omitempty"`
	RetryCount   int            `json:"retry_count,omitempty"`
}

type TaskListResponse struct {
	Tasks      []TaskResponse               `json:"tasks"`
	Pagination *inventory.PaginationResults `json:"pagination"`
}

func BuildTaskListResponse(tasks []queue.Task, res *inventory.ListTaskResult, nodeMap map[int]*ent.Node, hasher hashid.Encoder) *TaskListResponse {
	return &TaskListResponse{
		Pagination: res.PaginationResults,
		Tasks: lo.Map(tasks, func(t queue.Task, index int) TaskResponse {
			var (
				node *ent.Node
				s    = t.Summarize(hasher)
			)

			if s.NodeID > 0 {
				node = nodeMap[s.NodeID]
			}
			return *BuildTaskResponse(t, node, hasher)
		}),
	}
}

func BuildTaskResponse(task queue.Task, node *ent.Node, hasher hashid.Encoder) *TaskResponse {
	model := task.Model()
	t := &TaskResponse{
		Status:    string(task.Status()),
		CreatedAt: model.CreatedAt,
		UpdatedAt: model.UpdatedAt,
		ID:        hashid.EncodeTaskID(hasher, task.ID()),
		Type:      task.Type(),
		Summary:   task.Summarize(hasher),
		Error:     auth.RedactSensitiveValues(model.PublicState.Error),
		ErrorHistory: lo.Map(model.PublicState.ErrorHistory, func(s string, index int) string {
			return auth.RedactSensitiveValues(s)
		}),
		Duration:   model.PublicState.ExecutedDuration.Milliseconds(),
		ResumeTime: model.PublicState.ResumeTime,
		RetryCount: model.PublicState.RetryCount,
	}

	if node != nil {
		t.Node = user.BuildNode(node, hasher)
	}

	return t
}

type UploadSessionResponse struct {
	SessionID      string         `json:"session_id"`
	UploadID       string         `json:"upload_id"`
	ChunkSize      int64          `json:"chunk_size"` // 分块大小，0 为部分快
	Expires        int64          `json:"expires"`    // 上传凭证过期时间， Unix 时间戳
	UploadURLs     []string       `json:"upload_urls,omitempty"`
	Credential     string         `json:"credential,omitempty"`
	AccessKey      string         `json:"ak,omitempty"`
	KeyTime        string         `json:"keyTime,omitempty"` // COS用有效期
	CompleteURL    string         `json:"completeURL,omitempty"`
	StoragePolicy  *StoragePolicy `json:"storage_policy,omitempty"`
	Uri            string         `json:"uri"`
	CallbackSecret string         `json:"callback_secret"`
	MimeType       string         `json:"mime_type,omitempty"`
	UploadPolicy   string         `json:"upload_policy,omitempty"`
}

func BuildUploadSessionResponse(session *fs.UploadCredential, hasher hashid.Encoder) *UploadSessionResponse {
	return &UploadSessionResponse{
		SessionID:      session.SessionID,
		ChunkSize:      session.ChunkSize,
		Expires:        session.Expires,
		UploadURLs:     session.UploadURLs,
		Credential:     session.Credential,
		CompleteURL:    session.CompleteURL,
		Uri:            session.Uri,
		UploadID:       session.UploadID,
		StoragePolicy:  BuildStoragePolicy(session.StoragePolicy, hasher),
		CallbackSecret: session.CallbackSecret,
		MimeType:       session.MimeType,
		UploadPolicy:   session.UploadPolicy,
	}
}

// WopiFileInfo Response for `CheckFileInfo`
type WopiFileInfo struct {
	// Required
	BaseFileName string
	Version      string
	Size         int64

	// Breadcrumb
	BreadcrumbBrandName  string
	BreadcrumbBrandUrl   string
	BreadcrumbFolderName string
	BreadcrumbFolderUrl  string

	// Post Message
	FileSharingPostMessage bool
	FileVersionPostMessage bool
	ClosePostMessage       bool
	PostMessageOrigin      string

	// Other miscellaneous properties
	FileNameMaxLength int
	LastModifiedTime  string

	// User metadata
	IsAnonymousUser  bool
	UserFriendlyName string
	UserId           string
	OwnerId          string

	// Permission
	ReadOnly                bool
	UserCanRename           bool
	UserCanReview           bool
	UserCanWrite            bool
	UserCanNotWriteRelative bool

	SupportsRename    bool
	SupportsReviewing bool
	SupportsUpdate    bool
	SupportsLocks     bool

	EnableShare bool
}

type ViewerSessionResponse struct {
	Session *manager.ViewerSession `json:"session"`
	WopiSrc string                 `json:"wopi_src,omitempty"`
}

type ListResponse struct {
	Files      []FileResponse               `json:"files"`
	Parent     FileResponse                 `json:"parent,omitempty"`
	Pagination *inventory.PaginationResults `json:"pagination"`
	Props      *fs.NavigatorProps           `json:"props"`
	// ContextHint is used to speed up following operations under this listed directory.
	// It persists some intermedia state so that the following request don't need to query database again.
	// All the operations under this directory that supports context hint should carry this value in header
	// as X-Cr-Context-Hint.
	ContextHint           *uuid.UUID          `json:"context_hint"`
	RecursionLimitReached bool                `json:"recursion_limit_reached,omitempty"`
	MixedType             bool                `json:"mixed_type"`
	SingleFileView        bool                `json:"single_file_view,omitempty"`
	StoragePolicy         *StoragePolicy      `json:"storage_policy,omitempty"`
	View                  *types.ExplorerView `json:"view,omitempty"`
}

type FileResponse struct {
	Type          int                 `json:"type"`
	ID            string              `json:"id"`
	Name          string              `json:"name"`
	CreatedAt     time.Time           `json:"created_at"`
	UpdatedAt     time.Time           `json:"updated_at"`
	Size          int64               `json:"size"`
	Metadata      map[string]string   `json:"metadata"`
	Path          string              `json:"path,omitempty"`
	Shared        bool                `json:"shared,omitempty"`
	Capability    *boolset.BooleanSet `json:"capability,omitempty"`
	Owned         bool                `json:"owned,omitempty"`
	PrimaryEntity string              `json:"primary_entity,omitempty"`

	FolderSummary *fs.FolderSummary `json:"folder_summary,omitempty"`
	ExtendedInfo  *ExtendedInfo     `json:"extended_info,omitempty"`
}

type ExtendedInfo struct {
	StoragePolicy *StoragePolicy      `json:"storage_policy,omitempty"`
	StorageUsed   int64               `json:"storage_used"`
	Shares        []Share             `json:"shares,omitempty"`
	Entities      []Entity            `json:"entities,omitempty"`
	View          *types.ExplorerView `json:"view,omitempty"`
	DirectLinks   []DirectLink        `json:"direct_links,omitempty"`
}

type DirectLink struct {
	ID         string    `json:"id"`
	URL        string    `json:"url"`
	Downloaded int       `json:"downloaded"`
	CreatedAt  time.Time `json:"created_at"`
}

type StoragePolicy struct {
	ID                string           `json:"id"`
	Name              string           `json:"name"`
	AllowedSuffix     []string         `json:"allowed_suffix,omitempty"`
	DeniedSuffix      []string         `json:"denied_suffix,omitempty"`
	AllowedNameRegexp string           `json:"allowed_name_regexp,omitempty"`
	DeniedNameRegexp  string           `json:"denied_name_regexp,omitempty"`
	Type              types.PolicyType `json:"type"`
	MaxSize           int64            `json:"max_size"`
	Relay             bool             `json:"relay,omitempty"`
}

type Entity struct {
	ID            string           `json:"id"`
	Size          int64            `json:"size"`
	Type          types.EntityType `json:"type"`
	CreatedAt     time.Time        `json:"created_at"`
	StoragePolicy *StoragePolicy   `json:"storage_policy,omitempty"`
	CreatedBy     *user.User       `json:"created_by,omitempty"`
}

type Share struct {
	ID              string          `json:"id"`
	Name            string          `json:"name,omitempty"`
	RemainDownloads *int            `json:"remain_downloads,omitempty"`
	Visited         int             `json:"visited"`
	Downloaded      int             `json:"downloaded,omitempty"`
	Expires         *time.Time      `json:"expires,omitempty"`
	Unlocked        bool            `json:"unlocked"`
	SourceType      *types.FileType `json:"source_type,omitempty"`
	Owner           user.User       `json:"owner"`
	CreatedAt       time.Time       `json:"created_at,omitempty"`
	Expired         bool            `json:"expired"`
	Url             string          `json:"url"`
	ShowReadMe      bool            `json:"show_readme,omitempty"`

	// Only viewable by owner
	IsPrivate bool   `json:"is_private,omitempty"`
	Password  string `json:"password,omitempty"`
	ShareView bool   `json:"share_view,omitempty"`

	// Only viewable if explicitly unlocked by owner
	SourceUri string `json:"source_uri,omitempty"`
}

func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *ent.User, owner *ent.User,
	name string, t types.FileType, unlocked bool, expired bool) *Share {
	redactLevel := user.RedactLevelAnonymous
	if !inventory.IsAnonymousUser(requester) {
		redactLevel = user.RedactLevelUser
	}
	res := Share{
		Name:       name,
		ID:         hashid.EncodeShareID(hasher, s.ID),
		Unlocked:   unlocked,
		Owner:      user.BuildUserRedacted(owner, redactLevel, hasher),
		Expired:    inventory.IsShareExpired(s) != nil || expired,
		Url:        BuildShareLink(s, hasher, base),
		CreatedAt:  s.CreatedAt,
		Visited:    s.Views,
		SourceType: util.ToPtr(t),
	}

	if unlocked {
		res.RemainDownloads = s.RemainDownloads
		res.Downloaded = s.Downloads
		res.Expires = s.Expires
		res.Password = s.Password
		res.ShowReadMe = s.Props != nil && s.Props.ShowReadMe
	}

	if requester.ID == owner.ID {
		res.IsPrivate = s.Password != ""
		res.ShareView = s.Props != nil && s.Props.ShareView
	}

	return &res
}

func BuildListResponse(ctx context.Context, u *ent.User, parent fs.File, res *fs.ListFileResult, hasher hashid.Encoder) *ListResponse {
	r := &ListResponse{
		Files: lo.Map(res.Files, func(f fs.File, index int) FileResponse {
			return *BuildFileResponse(ctx, u, f, hasher, res.Props.Capability)
		}),
		Pagination:            res.Pagination,
		Props:                 res.Props,
		ContextHint:           res.ContextHint,
		RecursionLimitReached: res.RecursionLimitReached,
		MixedType:             res.MixedType,
		SingleFileView:        res.SingleFileView,
		StoragePolicy:         BuildStoragePolicy(res.StoragePolicy, hasher),
		View:                  res.View,
	}

	if !res.Parent.IsNil() {
		r.Parent = *BuildFileResponse(ctx, u, res.Parent, hasher, res.Props.Capability)
	}

	return r
}

func BuildFileResponse(ctx context.Context, u *ent.User, f fs.File, hasher hashid.Encoder, cap *boolset.BooleanSet) *FileResponse {
	var owner *ent.User
	if f != nil {
		owner = f.Owner()
	}

	if cap == nil {
		cap = f.Capabilities()
	}

	res := &FileResponse{
		Type:          int(f.Type()),
		ID:            hashid.EncodeFileID(hasher, f.ID()),
		Name:          f.DisplayName(),
		CreatedAt:     f.CreatedAt(),
		UpdatedAt:     f.UpdatedAt(),
		Size:          f.Size(),
		Metadata:      f.Metadata(),
		Path:          f.Uri(false).String(),
		Shared:        f.Shared(),
		Capability:    cap,
		Owned:         owner == nil || owner.ID == u.ID,
		FolderSummary: f.FolderSummary(),
		ExtendedInfo:  BuildExtendedInfo(ctx, u, f, hasher),
		PrimaryEntity: hashid.EncodeEntityID(hasher, f.PrimaryEntityID()),
	}
	return res
}

func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashid.Encoder) *ExtendedInfo {
	extendedInfo := f.ExtendedInfo()
	if extendedInfo == nil {
		return nil
	}

	dep := dependency.FromContext(ctx)
	base := dep.SettingProvider().SiteURL(ctx)

	ext := &ExtendedInfo{
		StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher),
		StorageUsed:   extendedInfo.StorageUsed,
		Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity {
			return BuildEntity(extendedInfo, e, hasher)
		}),
		DirectLinks: lo.Map(extendedInfo.DirectLinks, func(d *ent.DirectLink, index int) DirectLink {
			return BuildDirectLink(d, hasher, base)
		}),
	}

	if u.ID == f.OwnerID() {
		// Only owner can see the shares settings.
		ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share {
			return *BuildShare(s, base, hasher, u, u, f.DisplayName(), f.Type(), true, false)
		})
		ext.View = extendedInfo.View
	}

	return ext
}

func BuildDirectLink(d *ent.DirectLink, hasher hashid.Encoder, base *url.URL) DirectLink {
	return DirectLink{
		ID:         hashid.EncodeSourceLinkID(hasher, d.ID),
		URL:        routes.MasterDirectLink(base, hashid.EncodeSourceLinkID(hasher, d.ID), d.Name).String(),
		Downloaded: d.Downloads,
		CreatedAt:  d.CreatedAt,
	}
}

func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity {
	var u *user.User
	createdBy := e.CreatedBy()
	if createdBy != nil {
		userRedacted := user.BuildUserRedacted(e.CreatedBy(), user.RedactLevelAnonymous, hasher)
		u = &userRedacted
	}
	return Entity{
		ID:            hashid.EncodeEntityID(hasher, e.ID()),
		Type:          e.Type(),
		CreatedAt:     e.CreatedAt(),
		StoragePolicy: BuildStoragePolicy(extendedInfo.EntityStoragePolicies[e.PolicyID()], hasher),
		Size:          e.Size(),
		CreatedBy:     u,
	}
}

func BuildShareLink(s *ent.Share, hasher hashid.Encoder, base *url.URL) string {
	shareId := hashid.EncodeShareID(hasher, s.ID)
	return routes.MasterShareUrl(base, shareId, s.Password).String()
}

func BuildStoragePolicy(sp *ent.StoragePolicy, hasher hashid.Encoder) *StoragePolicy {
	if sp == nil {
		return nil
	}

	res := &StoragePolicy{
		ID:      hashid.EncodePolicyID(hasher, sp.ID),
		Name:    sp.Name,
		Type:    types.PolicyType(sp.Type),
		MaxSize: sp.MaxSize,
		Relay:   sp.Settings.Relay,
	}

	if sp.Settings.IsFileTypeDenyList {
		res.DeniedSuffix = sp.Settings.FileType
	} else {
		res.AllowedSuffix = sp.Settings.FileType
	}

	if sp.Settings.NameRegexp != "" {
		if sp.Settings.IsNameRegexpDenyList {
			res.DeniedNameRegexp = sp.Settings.NameRegexp
		} else {
			res.AllowedNameRegexp = sp.Settings.NameRegexp
		}
	}

	return res
}

func WriteEventSourceHeader(c *gin.Context) {
	c.Header("Content-Type", "text/event-stream")
	c.Header("Cache-Control", "no-cache")
	c.Header("X-Accel-Buffering", "no")
}

// WriteEventSource writes a Server-Sent Event to the client.
func WriteEventSource(c *gin.Context, event string, data any) {
	c.Writer.Write([]byte(fmt.Sprintf("event: %s\n", event)))
	c.Writer.Write([]byte("data:"))
	json.NewEncoder(c.Writer).Encode(data)
	c.Writer.Write([]byte("\n"))
	c.Writer.Flush()
}

var ErrSSETakeOver = errors.New("SSE take over")
