package server

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/c4pt0r/agfs/agfs-server/pkg/filesystem"

	"github.com/mem9-ai/drive9/pkg/backend"
	"github.com/mem9-ai/drive9/pkg/datastore"
	"github.com/mem9-ai/drive9/pkg/gitwsindex"
)

const (
	// Tenant-absolute remote existence index (shared wire type).
	gitWorkspaceIndexPath     = gitwsindex.Path
	maxGitWorkspaceIndexBytes = gitwsindex.MaxBytes
	gitWorkspaceIndexCASTries = 4
	// Do not pull a tenant-writable blob larger than this just to salvage extras.
	gitWorkspaceIndexSalvageCeiling = 2 * gitwsindex.MaxBytes
)

// removeGitWorkspaceIndexEntry removes workspaceID from the remote existence
// index with revision CAS retries. Missing index or already-absent entry is OK
// (idempotent). Corrupt/oversized documents are rebuilt from live rows inside
// the same retry loop; unknown fields are salvaged when the existing bytes
// are still valid JSON. A non-file node at the index path cannot be CAS-
// overwritten and is returned as a hard cleanup error. Emits a normal FS
// write so SSE subscribers observe the change.
func removeGitWorkspaceIndexEntry(ctx context.Context, b *backend.Dat9Backend, workspaceID string) error {
	if b == nil {
		return fmt.Errorf("backend is nil")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	if workspaceID == "" {
		return nil
	}
	if ctx == nil {
		ctx = context.Background()
	}
	var lastErr error
	for attempt := 0; attempt < gitWorkspaceIndexCASTries; attempt++ {
		nf, err := b.StatNodeCtx(ctx, gitWorkspaceIndexPath)
		if errors.Is(err, datastore.ErrNotFound) {
			return nil
		}
		if err != nil {
			return fmt.Errorf("stat git workspace index: %w", err)
		}
		if nf == nil || nf.Node.IsDirectory || nf.File == nil {
			// Directory or other non-file: Write(..., rev=0) is
			// create-if-absent and conflicts forever; a directory inode's
			// File can still be non-nil (size 0), so IsDirectory is required.
			return fmt.Errorf("git workspace index path is not a file")
		}
		rev := nf.File.Revision
		// Revision comes from Stat; body comes from a separate Read that
		// stats again internally. An interleaved writer produces a CAS
		// conflict and we retry. Do not fold this into a single call that
		// drops the revision used for the write.
		var salvage *gitwsindex.Index
		var body []byte
		if nf.File.SizeBytes > gitWorkspaceIndexSalvageCeiling {
			body, err = encodeRebuiltGitWorkspaceIndex(ctx, b, workspaceID, nil)
			if err != nil {
				return err
			}
		} else {
			data, err := b.ReadCtx(ctx, gitWorkspaceIndexPath, 0, maxGitWorkspaceIndexBytes+1)
			if errors.Is(err, datastore.ErrNotFound) {
				return nil
			}
			if err != nil {
				return fmt.Errorf("read git workspace index: %w", err)
			}
			idx, err := gitwsindex.Parse(data)
			if err == nil {
				if idx.Version == 0 {
					idx.Version = 1
				}
				out := make([]gitwsindex.Entry, 0, len(idx.Workspaces))
				found := false
				for _, e := range idx.Workspaces {
					if e.WorkspaceID == workspaceID {
						found = true
						continue
					}
					out = append(out, e)
				}
				if !found {
					return nil
				}
				idx.Workspaces = out
				idx.UpdatedAt = time.Now().UTC().Format(time.RFC3339Nano)
				body, err = json.MarshalIndent(idx, "", "  ")
				if err != nil {
					return fmt.Errorf("encode git workspace index: %w", err)
				}
			} else {
				if !errors.Is(err, gitwsindex.ErrUnreadable) {
					return err
				}
				// Oversize/entry-count: ParseRelaxed can still yield extras.
				// Malformed JSON: salvage stays nil and rebuild is lossy.
				salvage, _ = gitwsindex.ParseRelaxed(data)
				body, err = encodeRebuiltGitWorkspaceIndex(ctx, b, workspaceID, salvage)
				if err != nil {
					return err
				}
			}
		}
		// Empty document is kept via CAS write (same as client) so concurrent
		// upserts are not lost to an unconditional delete race.
		_, _, err = b.WriteCtxIfRevisionWithTagsResult(
			ctx,
			gitWorkspaceIndexPath,
			body,
			0,
			filesystem.WriteFlagCreate|filesystem.WriteFlagTruncate,
			rev,
			nil,
			"",
		)
		if err == nil {
			return nil
		}
		lastErr = err
		if errors.Is(err, datastore.ErrRevisionConflict) && attempt+1 < gitWorkspaceIndexCASTries {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(time.Duration(50*(attempt+1)) * time.Millisecond):
			}
			continue
		}
		return fmt.Errorf("write git workspace index: %w", err)
	}
	return fmt.Errorf("write git workspace index: %w", lastErr)
}

func encodeRebuiltGitWorkspaceIndex(ctx context.Context, b *backend.Dat9Backend, excludeID string, salvage *gitwsindex.Index) ([]byte, error) {
	if b == nil || b.Store() == nil {
		return nil, fmt.Errorf("backend store is nil")
	}
	live, err := b.Store().ListGitWorkspaces(ctx)
	if err != nil {
		return nil, fmt.Errorf("list git workspaces for index rebuild: %w", err)
	}
	idx := gitwsindex.Index{
		Version:    1,
		UpdatedAt:  time.Now().UTC().Format(time.RFC3339Nano),
		Workspaces: make([]gitwsindex.Entry, 0, len(live)),
	}
	for i := range live {
		ws := live[i]
		if ws.WorkspaceID == excludeID {
			continue
		}
		idx.Workspaces = append(idx.Workspaces, gitwsindex.Entry{
			WorkspaceID:   ws.WorkspaceID,
			RootPath:      ws.RootPath,
			WorkspaceKind: string(ws.Kind),
		})
	}
	idx.AbsorbUnknown(salvage)
	body, err := json.MarshalIndent(idx, "", "  ")
	if err != nil {
		return nil, fmt.Errorf("encode rebuilt git workspace index: %w", err)
	}
	return body, nil
}
