/*
 * 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 (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"

	"mstore/pkg/auxoci"
	"mstore/pkg/descr"
)

type ManifestExistsParams struct {
	Name      string
	Reference string
}
type ManifestExistsResult struct {
	ContentLength       string
	ContentType         string
	DockerContentDigest string
	Exists              bool
}

func (oper *Operator) ManifestExists(ctx context.Context, params *ManifestExistsParams) (*ManifestExistsResult, int, error) {
	var err error
	res := &ManifestExistsResult{}

	if params.Name == "" {
		err = fmt.Errorf("Empty name")
		return res, http.StatusBadRequest, err
	}
	if params.Reference == "" {
		err = fmt.Errorf("Empty reference")
		return res, http.StatusBadRequest, err
	}

	var manifest descr.Manifest
	var exists bool
	if stringLikeSHA256Digest(params.Reference) {
		digest := fmt.Sprintf("%s:%s", sha256prefix, params.Reference)
		exists, manifest, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !exists {
			return res, http.StatusNotFound, err
		}
	} else {
		exists, manifest, err = oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !exists {
			return res, http.StatusNotFound, err
		}
	}

	digest := auxoci.SHA256DigestFromString(manifest.Payload)
	payloadSize := len(manifest.Payload)
	res.ContentLength = strconv.FormatInt(int64(payloadSize), 10)
	res.ContentType = manifest.ContentType
	res.DockerContentDigest = digest.String()
	res.Exists = exists

	return res, http.StatusOK, err
}

type PutManifestParams struct {
	ContentType   string
	ContentLength string
	Name          string
	Reference     string
	Reader        io.Reader
}
type PutManifestResult struct {
	Location string
}

const (
	ddmMimeType = "application/vnd.docker.distribution.manifest.v2+json"
	oimMimeType = "application/vnd.oci.image.manifest.v1+json"

	oicMimeType = "application/vnd.oci.image.config.v1+json"
	dciMimeType = "application/vnd.docker.container.image.v1+json"
)

// TODO: lock for the name-reference or simular?
func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams) (*PutManifestResult, int, error) {
	var err error
	res := &PutManifestResult{}

	if params.Reference == "" {
		err = fmt.Errorf("Empty reference")
		return res, http.StatusBadRequest, err
	}
	if params.Name == "" {
		err = fmt.Errorf("Empty name")
		return res, http.StatusBadRequest, err
	}
	// Check Content-Type
	var mimeIsAcceptably bool
	mimeIsAcceptably = mimeIsAcceptably || params.ContentType == oimMimeType
	mimeIsAcceptably = mimeIsAcceptably || params.ContentType == ddmMimeType
	mimeIsAcceptably = mimeIsAcceptably || params.ContentType == oicMimeType
	mimeIsAcceptably = mimeIsAcceptably || params.ContentType == dciMimeType
	if !mimeIsAcceptably {
		err = fmt.Errorf("Unknown or empty Content-Type: %s", params.ContentType)
		return res, http.StatusNotFound, err
	}
	if params.ContentLength == "" {
		code := http.StatusLengthRequired
		err = fmt.Errorf("Content-Length is empty")
		return res, code, err
	}
	contentLength, err := strconv.ParseInt(params.ContentLength, 10, 64)
	if err != nil {
		err = fmt.Errorf("Cannot parse Content-Length value [%s]: %v", params.ContentLength, err)
		code := http.StatusLengthRequired
		return res, code, err
	}

	// Copy manifest data
	buffer := bytes.NewBuffer(nil)
	_, err = io.Copy(buffer, params.Reader)
	if err != nil {
		return res, http.StatusInternalServerError, err
	}

	incomingManifestBytes := buffer.Bytes()
	if int64(len(incomingManifestBytes)) != contentLength {
		err = fmt.Errorf("Mismatch Content-Length and received manifest size: %d vs %d",
			contentLength, len(incomingManifestBytes))
		code := http.StatusInternalServerError
		return res, code, err
	}

	if len(incomingManifestBytes) > (4 * 1024 * 1024) {
		err = fmt.Errorf("Payload more 4M: %d bytes", len(incomingManifestBytes))
		code := http.StatusRequestEntityTooLarge
		return res, code, err
	}

	// Check point
	oper.logg.Debugf("=== Incoming manifest type: %s", params.ContentType)
	oper.logg.Debugf("=== Incoming manifest body: %s", string(incomingManifestBytes))
	if params.ContentType == oicMimeType || params.ContentType == dciMimeType {
		return res, http.StatusOK, err
	}

	incomingManifest, err := auxoci.ParseOCIManifest(incomingManifestBytes)
	if err != nil {
		err = fmt.Errorf("Parsing OCI manifest error: %v", err)
		return res, http.StatusInternalServerError, err
	}
	if incomingManifest.MediaType == "" {
		incomingManifest.MediaType = params.ContentType
	}

	manifestExists, existengManifestDescr, err := oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference)
	if err != nil {
		return res, http.StatusInternalServerError, err
	}

	name := params.Name
	reference := params.Reference

	incomingManifestDescr, incomingLayerDescrs, err := descrsFromManifest(name, reference, incomingManifest, incomingManifestBytes)
	// Always check layer files for availability
	var blobError error
	for _, blobDescr := range incomingLayerDescrs {
		blobExists, _, err := oper.store.BlobExists(blobDescr.Digest)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !blobExists {
			oper.logg.Warningf("Found incomleted blob binary for %s:%s", blobDescr.Name, blobDescr.Digest)
			err := fmt.Errorf("Layer %s not found.", blobDescr.Digest)
			blobError = errors.Join(blobError, err)
		}
	}
	if blobError != nil {
		// TODO: more relevant code?
		return res, http.StatusFailedDependency, blobError
	}
	if !manifestExists {
		// Store manifest and layesrs data
		err = oper.mdb.InsertManifestWithLayers(ctx, &incomingManifestDescr, incomingLayerDescrs)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
	} else {
		/* TODO: only update descr
		   if bytes.Equal(existingManifestBytes, incomingManifestBytes) {
		      return res, http.StatusCreated, err
		   }
		*/

		existingManifestBytes := []byte(existengManifestDescr.Payload)
		existingManifest, err := auxoci.ParseOCIManifest(existingManifestBytes)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		addedBlobDescrs, uselessBlobDescrs, err := layersDiff(name, reference,
			existingManifest, incomingManifest, incomingManifestBytes)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		// Starting manifest and blobs transaction
		err = oper.mdb.UpdateManifestWithBlobs(ctx, &incomingManifestDescr, addedBlobDescrs, uselessBlobDescrs)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		for _, blob := range uselessBlobDescrs {
			exists, _, err := oper.store.BlobExists(blob.Digest)
			if err != nil {
				return res, http.StatusInternalServerError, err
			}
			blobUsage, err := oper.mdb.GetBlobUsage(ctx, blob.Digest)
			if err != nil {
				return res, http.StatusInternalServerError, err
			}
			if exists && blobUsage == 0 {
				err = oper.store.DeleteBlob(blob.Digest)
				if err != nil {
					return res, http.StatusInternalServerError, err
				}
			}
		}
	}

	for _, blobDescr := range incomingLayerDescrs {
		// TODO: move the requests to db layer transaction
		blobDescrExists, _, err := oper.mdb.GetBlobByNameDigest(ctx, blobDescr.Name, blobDescr.Digest)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !blobDescrExists {
			oper.logg.Warningf("Save incomleted blob descriptor for %s:%s", blobDescr.Name, blobDescr.Digest)
			err = oper.mdb.InsertBlob(ctx, &blobDescr)
			if err != nil {
				return res, http.StatusInternalServerError, err
			}
		}
	}

	res.Location = fmt.Sprintf(`/v2/%s/manifests/%s`, params.Name, params.Reference)
	return res, http.StatusCreated, err

}

type GetManifestParams struct {
	Name      string
	Reference string
}
type GetManifestResult struct {
	ContentLength       string
	ContentType         string
	DockerContentDigest string
	Payload             string
}

func (oper *Operator) GetManifest(ctx context.Context, params *GetManifestParams) (*GetManifestResult, int, error) {
	var err error
	res := &GetManifestResult{}

	if params.Name == "" {
		err = fmt.Errorf("Empty name")
		return res, http.StatusBadRequest, err
	}
	if params.Reference == "" {
		err = fmt.Errorf("Empty reference")
		return res, http.StatusBadRequest, err
	}

	manifestDescr := descr.Manifest{}
	var exists bool
	// TODO: checking layers?
	if stringLikeSHADigest(params.Reference) {
		digest := normalizeSHADigest(params.Reference)
		exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !exists {
			return res, http.StatusNotFound, err
		}
	} else {
		exists, manifestDescr, err = oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !exists {
			return res, http.StatusNotFound, err
		}
	}

	manifestDigest := auxoci.SHA256DigestFromString(manifestDescr.Payload)
	res.DockerContentDigest = manifestDigest.String()

	res.ContentLength = strconv.FormatInt(int64(len(manifestDescr.Payload)), 10)
	res.ContentType = manifestDescr.ContentType
	res.Payload = manifestDescr.Payload

	return res, http.StatusOK, err
}

type DeleteManifestParams struct {
	Name      string
	Reference string
}
type DeleteManifestResult struct{}

func (oper *Operator) DeleteManifest(ctx context.Context, params *DeleteManifestParams) (*DeleteManifestResult, int, error) {
	var err error
	res := &DeleteManifestResult{}

	if params.Name == "" {
		err = fmt.Errorf("Empty name")
		return res, http.StatusBadRequest, err
	}
	if params.Reference == "" {
		err = fmt.Errorf("Empty reference")
		return res, http.StatusBadRequest, err
	}

	var exists bool
	var reference string

	manifestDescr := descr.Manifest{}

	// Check manifest by digest as name
	if stringLikeSHADigest(params.Reference) {
		digest := normalizeSHADigest(params.Reference)
		exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !exists {
			return res, http.StatusNotFound, err
		}
		reference = manifestDescr.Reference
	} else {
		// Check manifest by name and reference
		exists, manifestDescr, err = oper.mdb.GetManifestByReference(ctx, params.Name, params.Reference)
		if err != nil {
			return res, http.StatusInternalServerError, err
		}
		if !exists {
			return res, http.StatusNotFound, err
		}
		reference = params.Reference
	}
	// Get blobs associated with the name
	err = oper.deleteManifestObjects(ctx, params.Name, reference)
	if err != nil {
		return res, http.StatusInternalServerError, err
	}
	return res, http.StatusAccepted, err
}

func (oper *Operator) deleteManifestObjects(ctx context.Context, name, reference string) error {
	var err error
	// Get blobs associated with the name
	layers, err := oper.mdb.GetBlobsByReferense(ctx, name, reference)
	if err != nil {
		return err
	}
	for _, blob := range layers {
		// Delete descr record
		err = oper.mdb.DeleteBlobByNameDigest(ctx, blob.Name, blob.Digest)
		if err != nil {
			return err
		}
		// Check blob file
		exists, _, err := oper.store.BlobExists(blob.Digest)
		if err != nil {
			return err
		}
		if exists {
			// Check blob usage
			blobUsage, err := oper.mdb.GetBlobUsage(ctx, blob.Digest)
			if err != nil {
				return err
			}
			// Delete if blob useless
			if blobUsage == 0 {
				err = oper.store.DeleteBlob(blob.Digest)
				if err != nil {
					return err
				}
			}
		}
	}
	err = oper.mdb.DeleteManifest(ctx, name, reference)
	if err != nil {
		return err
	}
	return err
}

type GetTagsParams struct {
	Name string
}
type GetTagsResult struct {
	TagDescr descr.Tags
}

func (oper *Operator) GetTags(ctx context.Context, params *GetTagsParams) (*GetTagsResult, int, error) {
	var err error
	res := &GetTagsResult{
		TagDescr: descr.Tags{
			Name: params.Name,
			Tags: make([]string, 0),
		},
	}

	if params.Name == "" {
		err = fmt.Errorf("Empty name")
		return res, http.StatusBadRequest, err
	}
	manifestDescrs, err := oper.mdb.ListManifestsByName(ctx, params.Name)
	if err != nil {
		return res, http.StatusInternalServerError, err
	}
	for _, manifestDescr := range manifestDescrs {
		res.TagDescr.Tags = append(res.TagDescr.Tags, manifestDescr.Reference)
	}
	return res, http.StatusOK, err
}

type GetRefererParams struct {
	Name   string
	Digest string
}
type GetRefererResult struct {
	Reference string
}

func (oper *Operator) GetReferer(ctx context.Context, params *GetRefererParams) (*GetRefererResult, int, error) {
	var err error
	res := &GetRefererResult{}

	if params.Name == "" {
		err = fmt.Errorf("Empty name")
		return res, http.StatusBadRequest, err
	}
	if params.Digest == "" {
		err = fmt.Errorf("Empty digest")
		return res, http.StatusBadRequest, err
	}
	manifests, err := oper.mdb.GetReferer(ctx, params.Name, params.Digest)
	if err != nil {
		return res, http.StatusInternalServerError, err
	}
	if len(manifests) == 0 {
		return res, http.StatusNotFound, err
	}
	res.Reference = manifests[0].Reference
	return res, http.StatusOK, err
}
