package oci

import (
	"bytes"
	"io"
	"net/http"
	"net/http/httptest"
	"sort"
	"sync"

	"github.com/go-chi/chi/v5"

	"git.wntrmute.dev/kyle/mcr/internal/auth"
	"git.wntrmute.dev/kyle/mcr/internal/db"
	"git.wntrmute.dev/kyle/mcr/internal/policy"
	"git.wntrmute.dev/kyle/mcr/internal/storage"
)

// manifestKey uniquely identifies a manifest for test lookup.
type manifestKey struct {
	repoID    int64
	reference string // tag or digest
}

// fakeDB implements DBQuerier for tests.
type fakeDB struct {
	mu        sync.Mutex
	repos     map[string]int64                // name -> id
	manifests map[manifestKey]*db.ManifestRow // (repoID, ref) -> manifest
	blobs     map[int64]map[string]bool       // repoID -> set of digests
	allBlobs  map[string]bool                 // global blob digests
	tags      map[int64][]string              // repoID -> sorted tag names
	repoNames []string                        // sorted repo names
	uploads   map[string]*db.UploadRow        // uuid -> upload
	nextID    int64                           // auto-increment counter
	pushed    []db.PushManifestParams         // record of pushed manifests
}

func newFakeDB() *fakeDB {
	return &fakeDB{
		repos:     make(map[string]int64),
		manifests: make(map[manifestKey]*db.ManifestRow),
		blobs:     make(map[int64]map[string]bool),
		allBlobs:  make(map[string]bool),
		tags:      make(map[int64][]string),
		uploads:   make(map[string]*db.UploadRow),
		nextID:    1,
	}
}

func (f *fakeDB) GetRepositoryByName(name string) (int64, error) {
	id, ok := f.repos[name]
	if !ok {
		return 0, db.ErrRepoNotFound
	}
	return id, nil
}

func (f *fakeDB) GetManifestByTag(repoID int64, tag string) (*db.ManifestRow, error) {
	m, ok := f.manifests[manifestKey{repoID, tag}]
	if !ok {
		return nil, db.ErrManifestNotFound
	}
	return m, nil
}

func (f *fakeDB) GetManifestByDigest(repoID int64, digest string) (*db.ManifestRow, error) {
	m, ok := f.manifests[manifestKey{repoID, digest}]
	if !ok {
		return nil, db.ErrManifestNotFound
	}
	return m, nil
}

func (f *fakeDB) BlobExistsInRepo(repoID int64, digest string) (bool, error) {
	digests, ok := f.blobs[repoID]
	if !ok {
		return false, nil
	}
	return digests[digest], nil
}

func (f *fakeDB) ListTags(repoID int64, after string, limit int) ([]string, error) {
	allTags := f.tags[repoID]
	var result []string
	for _, t := range allTags {
		if after != "" && t <= after {
			continue
		}
		result = append(result, t)
		if len(result) >= limit {
			break
		}
	}
	return result, nil
}

func (f *fakeDB) ListRepositoryNames(after string, limit int) ([]string, error) {
	var result []string
	for _, n := range f.repoNames {
		if after != "" && n <= after {
			continue
		}
		result = append(result, n)
		if len(result) >= limit {
			break
		}
	}
	return result, nil
}

func (f *fakeDB) GetOrCreateRepository(name string) (int64, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	id, ok := f.repos[name]
	if ok {
		return id, nil
	}
	id = f.nextID
	f.nextID++
	f.repos[name] = id
	f.repoNames = append(f.repoNames, name)
	sort.Strings(f.repoNames)
	return id, nil
}

func (f *fakeDB) BlobExists(digest string) (bool, error) {
	return f.allBlobs[digest], nil
}

func (f *fakeDB) InsertBlob(digest string, size int64) error {
	f.allBlobs[digest] = true
	_ = size
	return nil
}

func (f *fakeDB) PushManifest(p db.PushManifestParams) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.pushed = append(f.pushed, p)
	// Simulate creating the manifest in our fake data.
	repoID, ok := f.repos[p.RepoName]
	if !ok {
		repoID = f.nextID
		f.nextID++
		f.repos[p.RepoName] = repoID
		f.repoNames = append(f.repoNames, p.RepoName)
		sort.Strings(f.repoNames)
	}
	m := &db.ManifestRow{
		ID:           f.nextID,
		RepositoryID: repoID,
		Digest:       p.Digest,
		MediaType:    p.MediaType,
		Content:      p.Content,
		Size:         p.Size,
	}
	f.nextID++
	f.manifests[manifestKey{repoID, p.Digest}] = m
	if p.Tag != "" {
		f.manifests[manifestKey{repoID, p.Tag}] = m
	}
	return nil
}

func (f *fakeDB) CreateUpload(uuid string, repoID int64) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.uploads[uuid] = &db.UploadRow{
		ID:           f.nextID,
		UUID:         uuid,
		RepositoryID: repoID,
		ByteOffset:   0,
	}
	f.nextID++
	return nil
}

func (f *fakeDB) GetUpload(uuid string) (*db.UploadRow, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	u, ok := f.uploads[uuid]
	if !ok {
		return nil, db.ErrUploadNotFound
	}
	return u, nil
}

func (f *fakeDB) UpdateUploadOffset(uuid string, offset int64) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	u, ok := f.uploads[uuid]
	if !ok {
		return db.ErrUploadNotFound
	}
	u.ByteOffset = offset
	return nil
}

func (f *fakeDB) DeleteUpload(uuid string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	if _, ok := f.uploads[uuid]; !ok {
		return db.ErrUploadNotFound
	}
	delete(f.uploads, uuid)
	return nil
}

func (f *fakeDB) DeleteManifest(repoID int64, digest string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	key := manifestKey{repoID, digest}
	if _, ok := f.manifests[key]; !ok {
		return db.ErrManifestNotFound
	}
	delete(f.manifests, key)
	// Also remove any tag entries pointing to this digest.
	for k, m := range f.manifests {
		if k.repoID == repoID && m.Digest == digest {
			delete(f.manifests, k)
		}
	}
	return nil
}

func (f *fakeDB) DeleteBlobFromRepo(repoID int64, digest string) error {
	f.mu.Lock()
	defer f.mu.Unlock()
	digests, ok := f.blobs[repoID]
	if !ok || !digests[digest] {
		return db.ErrBlobNotFound
	}
	delete(digests, digest)
	return nil
}

// addRepo adds a repo to the fakeDB and returns its ID.
func (f *fakeDB) addRepo(name string, id int64) {
	f.repos[name] = id
	f.repoNames = append(f.repoNames, name)
	sort.Strings(f.repoNames)
	if id >= f.nextID {
		f.nextID = id + 1
	}
}

// addManifest adds a manifest accessible by both tag and digest.
func (f *fakeDB) addManifest(repoID int64, tag, digest, mediaType string, content []byte) {
	m := &db.ManifestRow{
		ID:           f.nextID,
		RepositoryID: repoID,
		Digest:       digest,
		MediaType:    mediaType,
		Content:      content,
		Size:         int64(len(content)),
	}
	f.nextID++
	if tag != "" {
		f.manifests[manifestKey{repoID, tag}] = m
	}
	f.manifests[manifestKey{repoID, digest}] = m
}

// addBlob registers a blob digest in a repository.
func (f *fakeDB) addBlob(repoID int64, digest string) {
	if f.blobs[repoID] == nil {
		f.blobs[repoID] = make(map[string]bool)
	}
	f.blobs[repoID][digest] = true
}

// addGlobalBlob registers a blob in the global blob table.
func (f *fakeDB) addGlobalBlob(digest string) {
	f.allBlobs[digest] = true
}

// addTag adds a tag to a repository's tag list.
func (f *fakeDB) addTag(repoID int64, tag string) {
	f.tags[repoID] = append(f.tags[repoID], tag)
	sort.Strings(f.tags[repoID])
}

// fakeBlobs implements BlobStore for tests.
type fakeBlobs struct {
	data    map[string][]byte // digest -> content
	uploads map[string]*bytes.Buffer
}

func newFakeBlobs() *fakeBlobs {
	return &fakeBlobs{
		data:    make(map[string][]byte),
		uploads: make(map[string]*bytes.Buffer),
	}
}

func (f *fakeBlobs) Open(digest string) (io.ReadCloser, error) {
	data, ok := f.data[digest]
	if !ok {
		return nil, io.ErrUnexpectedEOF
	}
	return io.NopCloser(bytes.NewReader(data)), nil
}

func (f *fakeBlobs) Stat(digest string) (int64, error) {
	data, ok := f.data[digest]
	if !ok {
		return 0, io.ErrUnexpectedEOF
	}
	return int64(len(data)), nil
}

func (f *fakeBlobs) StartUpload(uuid string) (*storage.BlobWriter, error) {
	// For tests that need real storage, use a real Store in t.TempDir().
	// This fake panics to catch unintended usage.
	panic("fakeBlobs.StartUpload should not be called; use a real storage.Store for upload tests")
}

// fakePolicy implements PolicyEval, always returning Allow.
type fakePolicy struct {
	effect policy.Effect
}

func (f *fakePolicy) Evaluate(_ policy.PolicyInput) (policy.Effect, *policy.Rule) {
	return f.effect, nil
}

// allowAll returns a fakePolicy that allows all requests.
func allowAll() *fakePolicy {
	return &fakePolicy{effect: policy.Allow}
}

// testRouter creates a chi.Mux with the OCI handler mounted at /v2.
func testRouter(h *Handler) *chi.Mux {
	parent := chi.NewRouter()
	parent.Mount("/v2", h.Router())
	return parent
}

// authedRequest creates an HTTP request with authenticated claims in the context.
func authedRequest(method, path string, body io.Reader) *http.Request {
	req := httptest.NewRequest(method, path, body)
	claims := &auth.Claims{
		Subject:     "test-user",
		AccountType: "human",
		Roles:       []string{"user"},
	}
	return req.WithContext(auth.ContextWithClaims(req.Context(), claims))
}
