/*
 * Copyright 2026 Oleg Borodin <onborodin@gmail.com>
 *
 * This work is published and licensed under a Creative Commons
 * Attribution-NonCommercial-NoDerivatives 4.0 International License.
 *
 * Distribution of this work is permitted, but commercial use and
 * modifications are strictly prohibited.
 */
package operator

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"path"
	"path/filepath"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"mstore/pkg/auxtool"
	"mstore/pkg/descr"
	"mstore/pkg/term"
	"mstore/pkg/uuid"
)

// FileInfo
type FileInfoParams struct {
	Filepath string
	Source   string
	Dest     string
}
type FileInfoResult struct {
	ContentCollection string
	ContentName       string
	ContentType       string
	ContentSize       string
	ContentDigest     string

	ContentCreatedAt string
	ContentCreatedBy uuid.UUID
	ContentUpdatedAt string
	ContentUpdatedBy uuid.UUID
}

func cleanFilepath(filename string) (string, error) {
	filename = "/" + filename
	return filepath.Clean(filename), nil
}

func (oper *Operator) FileInfo(ctx context.Context, operatorID uuid.UUID, param *FileInfoParams) (int, *FileInfoResult, error) {
	var err error
	code := http.StatusOK
	res := &FileInfoResult{}

	xfilepath, err := cleanFilepath(param.Filepath)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}

	filename := path.Base(xfilepath)
	collection := path.Dir(xfilepath)

	exist, fileDescr, err := oper.mdb.GetFileByCollectionName(ctx, collection, filename)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	if !exist {
		code = http.StatusNotFound
		return code, res, err
	}
	res = &FileInfoResult{
		ContentCollection: fileDescr.Collection,
		ContentName:       fileDescr.Name,
		ContentSize:       strconv.FormatInt(fileDescr.Size, 10),
		ContentType:       fileDescr.Type,
		ContentDigest:     fileDescr.Checksum,

		ContentCreatedAt: fileDescr.CreatedAt,
		ContentCreatedBy: fileDescr.CreatedBy,
		ContentUpdatedAt: fileDescr.UpdatedAt,
		ContentUpdatedBy: fileDescr.UpdatedBy,
	}
	return code, res, err
}

// PutFile
type PutFileParams struct {
	ContentType string
	ContentSize string
	Filepath    string
	Source      io.ReadCloser
}
type PutFileResult struct{}

const defaultContentType = "application/octet-stream"

// TODO: checking catalog and file names conflict
func (oper *Operator) PutFile(ctx context.Context, operatorID uuid.UUID, param *PutFileParams) (int, *PutFileResult, error) {
	var err error
	res := &PutFileResult{}

	if param.ContentSize == "" {
		code := http.StatusLengthRequired
		err = fmt.Errorf("Required Content-Size header is empty")
		return code, res, err
	}
	size, err := strconv.ParseInt(param.ContentSize, 10, 64)
	if err != nil {
		code := http.StatusLengthRequired
		return code, res, err
	}
	contentType := param.ContentType
	if contentType == "" {
		contentType = defaultContentType
	}

	// TODO: convert file path to a unified and secure state
	xfilepath, err := cleanFilepath(param.Filepath)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	filename := path.Base(xfilepath)
	collection := path.Dir(xfilepath)

	tmpname, size, checksum, err := oper.store.WriteTempFile(param.Source)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}

	descrExists, fileDescr, err := oper.mdb.GetFileByCollectionName(ctx, collection, filename)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}

	now := auxtool.TimeNow()
	if descrExists {
		fileDescr.Size = size
		fileDescr.Checksum = checksum
		fileDescr.UpdatedAt = now
		fileDescr.Type = contentType
		fileDescr.UpdatedBy = operatorID
		err = oper.mdb.UpdateFileByID(ctx, fileDescr.ID, fileDescr)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
	} else {
		fileDescr = &descr.File{
			ID:         uuid.NewUUID(),
			Name:       filename,
			Collection: collection,
			Size:       size,
			Type:       contentType,
			Checksum:   checksum,
			CreatedAt:  now,
			UpdatedAt:  now,
			CreatedBy:  operatorID,
			UpdatedBy:  operatorID,
		}
		err = oper.mdb.InsertFile(ctx, fileDescr)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
	}

	err = oper.store.HardlinkFile(tmpname, collection, filename)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	code := http.StatusOK
	return code, res, err
}

// GetFile
type GetFileParams struct {
	Filepath string
}
type GetFileResult struct {
	ContentType   string
	ContentSize   string
	ContentDigest string
	Source        io.ReadCloser

	ContentCreatedAt string
	ContentCreatedBy uuid.UUID
	ContentUpdatedAt string
	ContentUpdatedBy uuid.UUID
}

func (oper *Operator) GetFile(ctx context.Context, operatorID uuid.UUID, param *GetFileParams) (int, *GetFileResult, error) {
	var err error
	res := &GetFileResult{}

	// TODO: convert file path to a unified and secure state

	xfilepath, err := cleanFilepath(param.Filepath)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	filename := path.Base(xfilepath)
	collection := path.Dir(xfilepath)

	descrExists, fileDescr, err := oper.mdb.GetFileByCollectionName(ctx, collection, filename)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	if !descrExists {
		code := http.StatusNotFound
		return code, res, err
	}
	reader, err := oper.store.GetFileReader(collection, filename)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	res = &GetFileResult{
		ContentSize:   strconv.FormatInt(fileDescr.Size, 10),
		ContentType:   fileDescr.Type,
		ContentDigest: fileDescr.Checksum,
		Source:        reader,

		ContentCreatedAt: fileDescr.CreatedAt,
		ContentCreatedBy: fileDescr.CreatedBy,
		ContentUpdatedAt: fileDescr.UpdatedAt,
		ContentUpdatedBy: fileDescr.UpdatedBy,
	}
	code := http.StatusOK
	return code, res, err
}

// DeleteFile
type DeleteFileParams struct {
	Filepath string
}
type DeleteFileResult struct{}

func (oper *Operator) DeleteFile(ctx context.Context, operatorID uuid.UUID, param *DeleteFileParams) (int, *DeleteFileResult, error) {
	var err error
	res := &DeleteFileResult{}
	code := http.StatusOK

	xfilepath, err := cleanFilepath(param.Filepath)
	if err != nil {
		code := http.StatusInternalServerError
		return code, res, err
	}
	filename := path.Base(xfilepath)
	collection := path.Dir(xfilepath)

	descrExists, _, err := oper.mdb.GetFileByCollectionName(ctx, collection, filename)
	if err != nil {
		code = http.StatusInternalServerError
		return code, res, err
	}
	if !descrExists {
		code := http.StatusNotFound
		return code, res, err
	}

	if descrExists {
		err = oper.mdb.DeleteFileByCollectionName(ctx, collection, filename)
		if err != nil {
			code = http.StatusInternalServerError
			return code, res, err
		}
	}
	err = oper.store.DeleteFile(collection, filename)
	if err != nil {
		code = http.StatusInternalServerError
		return code, res, err
	}
	return code, res, err
}

// ListFiles
type ListFilesParams struct {
	Filepath string
	PathAs   term.PathUsage `param:"pathAs"`
}
type ListFilesResult struct {
	Files []descr.File `json:"files,omitempty"`
}

func (oper *Operator) ListFiles(ctx context.Context, operatorID uuid.UUID, params *ListFilesParams) (int, *ListFilesResult, error) {
	var err error
	res := &ListFilesResult{
		Files: make([]descr.File, 0),
	}
	switch params.PathAs {
	case term.AsRegexp:
		files, err := oper.listFilesWithRegex(ctx, params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		res.Files = files
	case term.AsPrefix:
		params.Filepath, err = cleanFilepath(params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		params.Filepath, err = cleanFilepath(params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}

		files, err := oper.listFilesWithPrefix(ctx, params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		res.Files = files
	default: // Fine
		params.Filepath, err = cleanFilepath(params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		params.Filepath, err = cleanFilepath(params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}

		files, err := oper.listFilesInOneCollection(ctx, params.Filepath)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		res.Files = files

	}
	code := http.StatusOK
	return code, res, err
}

func (oper *Operator) listFilesInOneCollection(ctx context.Context, collection string) ([]descr.File, error) {
	var err error
	res := make([]descr.File, 0)
	files, err := oper.mdb.ListFilesByCollection(ctx, collection)
	if err != nil {
		return res, err
	}
	res = files
	return res, err
}

func (oper *Operator) listFilesWithPrefix(ctx context.Context, prefix string) ([]descr.File, error) {
	var err error
	res := make([]descr.File, 0)
	files, err := oper.mdb.ListAllFiles(ctx)
	if err != nil {
		return res, err
	}
	for _, file := range files {
		fullpath := filepath.Join(file.Collection, file.Name)
		if strings.HasPrefix(fullpath, prefix) {
			res = append(res, file)
		}
	}
	return res, err
}

func (oper *Operator) listFilesWithRegex(ctx context.Context, regex string) ([]descr.File, error) {
	var err error
	res := make([]descr.File, 0)

	re, err := regexp.Compile(regex)
	if err != nil {
		return res, err
	}
	files, err := oper.mdb.ListAllFiles(ctx)
	if err != nil {
		return res, err
	}
	for _, file := range files {
		fullpath := filepath.Join(file.Collection, file.Name)
		if re.MatchString(fullpath) {
			res = append(res, file)
		}
	}
	return res, err
}

// ListCollections
type ListCollectionsParams struct {
	Path   string
	PathAS term.PathUsage `param:"pathAs"`
}
type ListCollectionsResult struct {
	Collections []string `json:"collection,omitempty"`
}

func (oper *Operator) ListCollections(ctx context.Context, operatorID uuid.UUID, param *ListCollectionsParams) (int, *ListCollectionsResult, error) {
	var err error
	res := &ListCollectionsResult{
		Collections: make([]string, 0),
	}

	collectionList := make([]string, 0)
	switch param.PathAS {
	case term.AsRegexp:
		collectionList, err = oper.listCollectionsWithRegexp(ctx, param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
	case term.AsPrefix:
		param.Path, err = cleanFilepath(param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}

		collectionList, err = oper.listCollectionsWithPrefix(ctx, param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
	default:
		param.Path, err = cleanFilepath(param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}

		collectionList, err = oper.listAllCollections(ctx)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}

	}
	res.Collections = collectionList
	code := http.StatusOK
	return code, res, err
}

func (oper *Operator) listCollectionsWithPrefix(ctx context.Context, prefix string) ([]string, error) {
	var err error
	res := make([]string, 0)

	fileDescrs, err := oper.mdb.ListAllFiles(ctx) // TODO
	if err != nil {
		return res, err
	}
	collMap := make(map[string]bool)
	for _, item := range fileDescrs {
		_, exists := collMap[item.Collection]
		if !exists {
			collMap[item.Collection] = true
		}
	}
	res = make([]string, 0)
	for key, _ := range collMap {
		if strings.HasPrefix(key, prefix) {
			res = append(res, key)
		}
	}
	slices.Sort(res)
	return res, err
}

func (oper *Operator) listCollectionsWithRegexp(ctx context.Context, regex string) ([]string, error) {
	var err error
	res := make([]string, 0)

	re, err := regexp.Compile(regex)
	if err != nil {
		return res, err
	}

	fileDescrs, err := oper.mdb.ListAllFiles(ctx) // TODO
	if err != nil {
		return res, err
	}
	collMap := make(map[string]bool)
	for _, item := range fileDescrs {
		_, exists := collMap[item.Collection]
		if !exists {
			collMap[item.Collection] = true
		}
	}
	res = make([]string, 0)
	for key, _ := range collMap {
		if re.MatchString(key) {
			res = append(res, key)
		}
	}
	slices.Sort(res)
	return res, err
}

func (oper *Operator) listAllCollections(ctx context.Context) ([]string, error) {
	var err error
	res := make([]string, 0)

	fileDescrs, err := oper.mdb.ListAllFiles(ctx) // TODO
	if err != nil {
		return res, err
	}
	collMap := make(map[string]bool)
	for _, item := range fileDescrs {
		_, exists := collMap[item.Collection]
		if !exists {
			collMap[item.Collection] = true
		}
	}
	res = make([]string, 0)
	for key, _ := range collMap {
		res = append(res, key)
	}
	slices.Sort(res)
	return res, err
}

// DeleteColletion
type DeleteColletionParams struct {
	Path   string
	PathAs term.PathUsage `param:"pathAs"`
	DryRun bool           `param:"dryRun"`
}
type DeleteColletionResult struct {
	Files []descr.File `json:"files,omitempty"`
}

func (oper *Operator) DeleteColletion(ctx context.Context, operatorID uuid.UUID, param *DeleteColletionParams) (int, *DeleteColletionResult, error) {
	var err error
	res := &DeleteColletionResult{
		Files: make([]descr.File, 0),
	}
	switch param.PathAs {
	case term.AsRegexp:
		collections, err := oper.listCollectionsWithRegexp(ctx, param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		allfiles := make([]descr.File, 0)
		for _, collection := range collections {
			files, err := oper.deleteFilesInCollection(ctx, collection, param.DryRun)
			if err != nil {
				code := http.StatusInternalServerError
				return code, res, err
			}
			allfiles = append(allfiles, files...)
		}
		res.Files = allfiles

	case term.AsPrefix:
		param.Path, err = cleanFilepath(param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}

		collections, err := oper.listCollectionsWithPrefix(ctx, param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		allfiles := make([]descr.File, 0)
		for _, collection := range collections {
			files, err := oper.deleteFilesInCollection(ctx, collection, param.DryRun)
			if err != nil {
				code := http.StatusInternalServerError
				return code, res, err
			}
			allfiles = append(allfiles, files...)
		}
		res.Files = allfiles
	default: // Fine
		param.Path, err = cleanFilepath(param.Path)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		collection := param.Path
		files, err := oper.deleteFilesInCollection(ctx, collection, param.DryRun)
		if err != nil {
			code := http.StatusInternalServerError
			return code, res, err
		}
		res.Files = files
	}
	code := http.StatusOK
	return code, res, err
}

func (oper *Operator) deleteFilesInCollection(ctx context.Context, collection string, dryRun bool) ([]descr.File, error) {
	var err error
	res := make([]descr.File, 0)
	files, err := oper.mdb.ListFilesByCollection(ctx, collection)
	if err != nil {
		return res, err
	}
	for _, file := range files {
		oper.logg.Debugf("Delete file %s/%s", file.Collection, file.Name)
		if !dryRun {
			err = oper.store.DeleteFile(file.Collection, file.Name)
			if err != nil {
				oper.logg.Warningf("%v", err)
				err = nil
			}
			err = oper.mdb.DeleteFileByCollectionName(ctx, file.Collection, file.Name)
			if err != nil {
				return res, err
			}
		}

		res = append(res, file)
	}
	return res, err
}
