package manifests

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strings"
	"sync"

	"nf-repo/internal/interfaces"
	"nf-repo/internal/model"
	"nf-repo/internal/model/types"
	"nf-repo/internal/tool/rerr"

	"github.com/loveuer/nf/nft/log"
)

type memManifest struct {
	sync.RWMutex
	m map[string]map[string]*model.RepoSimpleManifest
}

func (m *memManifest) Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) {
	m.RLock()
	defer m.RUnlock()

	digestToManifestMap, repoExists := m.m[repo]
	if !repoExists {
		return nil, &rerr.RepositoryError{
			Status:  http.StatusNotFound,
			Code:    "NAME_UNKNOWN",
			Message: "Unknown name",
		}
	}

	im := &model.IndexManifest{
		SchemaVersion: 2,
		MediaType:     types.OCIImageIndex,
		Manifests:     []model.Descriptor{},
	}

	for digest, manifest := range digestToManifestMap {
		h, err := model.NewHash(digest)
		if err != nil {
			continue
		}

		var refPointer struct {
			Subject *model.Descriptor `json:"subject"`
		}

		json.Unmarshal(manifest.Blob, &refPointer)
		if refPointer.Subject == nil {
			continue
		}

		referenceDigest := refPointer.Subject.Digest
		if referenceDigest.String() != target {
			continue
		}
		// At this point, we know the current digest references the target
		var imageAsArtifact struct {
			Config struct {
				MediaType string `json:"mediaType"`
			} `json:"config"`
		}

		json.Unmarshal(manifest.Blob, &imageAsArtifact)

		im.Manifests = append(im.Manifests, model.Descriptor{
			MediaType:    types.MediaType(manifest.ContentType),
			Size:         int64(len(manifest.Blob)),
			Digest:       h,
			ArtifactType: imageAsArtifact.Config.MediaType,
		})

	}

	return im, nil
}

func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int, keyword string) (*model.Tag, *rerr.RepositoryError) {
	m.RLock()
	defer m.RUnlock()

	c, ok := m.m[repo]
	if !ok {
		return nil, &rerr.RepositoryError{
			Status:  http.StatusNotFound,
			Code:    "NAME_UNKNOWN",
			Message: "Unknown name",
		}
	}

	var tags []string
	for tag := range c {
		if strings.Contains(tag, "sha256:") {
			continue
		}

		if keyword == "" {
			tags = append(tags, tag)
			continue
		}

		if strings.Contains(tag, keyword) {
			tags = append(tags, tag)
		}
	}

	sort.Strings(tags)

	// https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated
	// Offset using last query parameter.
	//if last := ctx.Query("last"); last != "" {
	//	for i, t := range tags {
	//		if t > last {
	//			tags = tags[i:]
	//			break
	//		}
	//	}
	//}
	//
	//// Limit using n query parameter.
	//if ns := ctx.Query("n"); ns != "" {
	//	if n, err := strconv.Atoi(ns); err != nil {
	//		return rerr.Error(ctx, &rerr.RepositoryError{
	//			Status:  http.StatusBadRequest,
	//			Code:    "BAD_REQUEST",
	//			Message: fmt.Sprintf("parsing n: %v", err),
	//		})
	//	} else if n < len(tags) {
	//		tags = tags[:n]
	//	}
	//}

	tagsToList := &model.Tag{
		Name: repo,
		Tags: tags,
	}

	return tagsToList, nil
}

func (m *memManifest) Catalog(ctx context.Context, limit, last int, keyword string) (*model.Catalog, *rerr.RepositoryError) {
	m.RLock()
	defer m.RUnlock()

	var repos []string
	countRepos := 0

	// TODO: implement pagination
	for key := range m.m {
		if countRepos >= limit {
			break
		}

		if keyword != "" && !strings.Contains(key, keyword) {
			continue
		}

		countRepos++

		repos = append(repos, key)
	}

	repositoriesToList := &model.Catalog{
		Repositories: repos,
	}

	return repositoriesToList, nil
}

func (m *memManifest) Put(ctx context.Context, repo string, target string, digest string, mf *model.RepoSimpleManifest) *rerr.RepositoryError {
	if types.MediaType(mf.ContentType).IsIndex() {
		if err := func() *rerr.RepositoryError {
			m.RLock()
			defer m.RUnlock()

			im, err := model.ParseIndexManifest(bytes.NewReader(mf.Blob))
			if err != nil {
				return &rerr.RepositoryError{
					Status:  http.StatusBadRequest,
					Code:    "MANIFEST_INVALID",
					Message: err.Error(),
				}
			}

			for _, desc := range im.Manifests {
				if !desc.MediaType.IsDistributable() {
					continue
				}

				if desc.MediaType.IsIndex() || desc.MediaType.IsImage() {
					if _, found := m.m[repo][desc.Digest.String()]; !found {
						return &rerr.RepositoryError{
							Status:  http.StatusNotFound,
							Code:    "MANIFEST_UNKNOWN",
							Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest),
						}
					}
				} else {
					// TODO: Probably want to do an existence check for blobs.
					log.Warn("TODO: Check blobs for %q", desc.Digest)
				}
			}
			return nil
		}(); err != nil {
			return err
		}
	}

	m.Lock()
	defer m.Unlock()

	if _, ok := m.m[repo]; !ok {
		m.m[repo] = make(map[string]*model.RepoSimpleManifest, 2)
	}

	// Allow future references by target (tag) and immutable digest.
	// See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier.
	m.m[repo][digest] = mf
	m.m[repo][target] = mf

	return nil
}

func (m *memManifest) Delete(ctx context.Context, repo string, target string) *rerr.RepositoryError {
	m.Lock()
	defer m.Unlock()

	if _, ok := m.m[repo]; !ok {
		return &rerr.RepositoryError{
			Status:  http.StatusNotFound,
			Code:    "NAME_UNKNOWN",
			Message: "Unknown name",
		}
	}

	_, ok := m.m[repo][target]
	if !ok {
		return &rerr.RepositoryError{
			Status:  http.StatusNotFound,
			Code:    "MANIFEST_UNKNOWN",
			Message: "Unknown manifest",
		}
	}

	delete(m.m[repo], target)

	if len(m.m[repo]) == 0 {
		delete(m.m, repo)
	}

	return nil
}

func (m *memManifest) Get(ctx context.Context, repo string, target string) (io.ReadCloser, string, *rerr.RepositoryError) {
	m.RLock()
	defer m.RUnlock()

	c, ok := m.m[repo]
	if !ok {
		return nil, "", &rerr.RepositoryError{
			Status:  http.StatusNotFound,
			Code:    "NAME_UNKNOWN",
			Message: "Unknown name",
		}
	}

	f, ok := c[target]
	if !ok {
		return nil, "", &rerr.RepositoryError{
			Status:  http.StatusNotFound,
			Code:    "MANIFEST_UNKNOWN",
			Message: "Unknown manifest",
		}
	}

	reader := io.NopCloser(bytes.NewReader(f.Blob))

	return reader, f.ContentType, nil
}

func NewManifestMemHandler() interfaces.ManifestHandler {
	return &memManifest{m: make(map[string]map[string]*model.RepoSimpleManifest)}
}