/*
 * 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 imageoper

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"

	"mstore/pkg/auxoci"
)

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"

	XXXoicMimeType = "application/vnd.oci.image.config.v1+json"
	XXXdciMimeType = "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
	}

	resName := params.Name
	oper.iLock.WaitAndLock(resName)
	defer oper.iLock.Done(resName)

	// 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
	}

	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
	}

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

	arch := incomingManifest.Subject.Platform.Architecture
	os := incomingManifest.Subject.Platform.OS
	variant := incomingManifest.Subject.Platform.Variant

	manifestExists, existingManifestDescr, err := oper.mdb.GetManifestsByReferenceArchitecture(ctx, name, reference, arch, os, variant)
	if err != nil {
		return res, http.StatusInternalServerError, err
	}

	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(existingManifestDescr.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
}
