package server

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	pathpkg "path"
	"sort"

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

type fsStatMetadataResponse struct {
	Size         int64             `json:"size"`
	IsDir        bool              `json:"isdir"`
	ResourceID   string            `json:"resource_id"`
	Nlink        uint32            `json:"nlink,omitempty"`
	Revision     int64             `json:"revision"`
	Mtime        *int64            `json:"mtime,omitempty"`
	ContentType  string            `json:"content_type"`
	SemanticText string            `json:"semantic_text"`
	Description  string            `json:"description"`
	Tags         map[string]string `json:"tags"`
}

type projectedDirectoryResult struct {
	Status int
	Err    error
}

func authorizeFSStat(w http.ResponseWriter, r *http.Request, rawPath string) (fsStatDecision, bool) {
	scope := ScopeFromContext(r.Context())
	if scope == nil {
		errJSON(w, http.StatusUnauthorized, "missing tenant scope")
		return fsStatDecision{}, false
	}
	decision, err := scope.resolveFSStat(rawPath)
	if err != nil {
		writeFSAuthzError(w, err)
		return fsStatDecision{}, false
	}
	if decision.Mode != fsStatDenied {
		return decision, true
	}
	if decision.Hidden {
		errJSON(w, http.StatusNotFound, "not found")
		return fsStatDecision{}, false
	}
	writeFSAuthzError(w, ErrFSAccessDenied)
	return fsStatDecision{}, false
}

func authorizeFSFullList(w http.ResponseWriter, r *http.Request, rawPath string) bool {
	scope := ScopeFromContext(r.Context())
	if scope == nil {
		errJSON(w, http.StatusUnauthorized, "missing tenant scope")
		return false
	}
	decision, err := scope.resolveFSList(rawPath)
	if err != nil {
		writeFSAuthzError(w, err)
		return false
	}
	if decision.Mode == fsListFull {
		return true
	}
	if decision.Hidden {
		errJSON(w, http.StatusNotFound, "not found")
		return false
	}
	writeFSAuthzError(w, ErrFSAccessDenied)
	return false
}

func verifyProjectedDirectory(w http.ResponseWriter, r *http.Request, b *backend.Dat9Backend, path string, decision fsStatDecision) bool {
	result := resolveProjectedDirectory(r.Context(), b, path, decision)
	if result.Status == http.StatusOK {
		return true
	}
	writeProjectedDirectoryError(w, r, result)
	return false
}

func resolveProjectedDirectory(ctx context.Context, b *backend.Dat9Backend, path string, decision fsStatDecision) projectedDirectoryResult {
	if decision.Boundary {
		return projectedDirectoryResult{Status: http.StatusOK}
	}
	nf, err := b.StatNodeLiteCtx(ctx, path)
	if err != nil {
		if errors.Is(err, datastore.ErrNotFound) {
			return projectedDirectoryResult{Status: http.StatusNotFound}
		}
		return projectedDirectoryResult{Status: http.StatusInternalServerError, Err: err}
	}
	if nf.Node.IsDirectory {
		return projectedDirectoryResult{Status: http.StatusOK}
	}
	if decision.HideNonDirectory {
		return projectedDirectoryResult{Status: http.StatusNotFound}
	}
	return projectedDirectoryResult{Status: http.StatusForbidden}
}

func writeProjectedDirectoryError(w http.ResponseWriter, r *http.Request, result projectedDirectoryResult) {
	switch result.Status {
	case http.StatusNotFound:
		errJSON(w, http.StatusNotFound, "not found")
	case http.StatusForbidden:
		writeFSAuthzError(w, ErrFSAccessDenied)
	default:
		if result.Err != nil {
			writeBackendError(w, r, result.Err)
			return
		}
		errJSON(w, http.StatusInternalServerError, "backend unavailable")
	}
}

func writeSyntheticStatMetadata(w http.ResponseWriter) {
	zero := int64(0)
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("X-Dat9-Mtime", "0")
	_ = json.NewEncoder(w).Encode(fsStatMetadataResponse{
		IsDir: true,
		Nlink: 2,
		Mtime: &zero,
		Tags:  make(map[string]string),
	})
}

func writeSyntheticHeadStat(w http.ResponseWriter) {
	w.Header().Set("Content-Length", "0")
	w.Header().Set("X-Dat9-IsDir", "true")
	w.Header().Set("X-Dat9-Mtime", "0")
	w.Header().Set("X-Dat9-Nlink", "2")
	w.WriteHeader(http.StatusOK)
}

func authorizeFSStatPathForBatch(scope *TenantScope, rawPath string) (decision fsStatDecision, status int, message string, allowed bool) {
	decision, err := scope.resolveFSStat(rawPath)
	if err != nil {
		switch {
		case errors.Is(err, ErrFSAccessDenied):
			return fsStatDecision{}, http.StatusForbidden, "fs access denied", false
		case errors.Is(err, ErrFSInvalidPath):
			return fsStatDecision{}, http.StatusBadRequest, "invalid fs path", false
		default:
			return fsStatDecision{}, http.StatusInternalServerError, "fs authorization failed", false
		}
	}
	if decision.Mode != fsStatDenied {
		return decision, 0, "", true
	}
	if decision.Hidden {
		return fsStatDecision{}, http.StatusNotFound, "not found", false
	}
	return fsStatDecision{}, http.StatusForbidden, "fs access denied", false
}

func projectedBatchStatOne(ctx context.Context, b *backend.Dat9Backend, rawPath string, decision fsStatDecision) batchStatResult {
	result := batchStatResult{Path: rawPath}
	resolved := resolveProjectedDirectory(ctx, b, rawPath, decision)
	if resolved.Status == http.StatusOK {
		result.Status = http.StatusOK
		result.IsDir = true
		result.Nlink = 2
		return result
	}
	result.Status = resolved.Status
	switch {
	case resolved.Err != nil:
		result.Error = resolved.Err.Error()
	case resolved.Status == http.StatusNotFound:
		result.Error = "not found"
	case resolved.Status == http.StatusForbidden:
		result.Error = "fs access denied"
	}
	return result
}

func projectedListEntries(ctx context.Context, b *backend.Dat9Backend, scope *TenantScope, path string, decision fsListDecision) ([]fsListResponseEntry, projectedDirectoryResult) {
	resolved := resolveProjectedDirectory(ctx, b, path, fsStatDecision{
		Mode:             fsStatProjected,
		Boundary:         decision.Boundary,
		HideNonDirectory: true,
	})
	if resolved.Status != http.StatusOK {
		return nil, resolved
	}

	names := make([]string, 0, len(decision.projectedChildren))
	for name := range decision.projectedChildren {
		names = append(names, name)
	}
	sort.Strings(names)
	out := make([]fsListResponseEntry, 0, len(names))
	for _, name := range names {
		childPath := pathpkg.Join(path, name)
		nf, err := b.StatNodeLiteCtx(ctx, childPath)
		if errors.Is(err, datastore.ErrNotFound) {
			continue
		}
		if err != nil {
			return nil, projectedDirectoryResult{Status: http.StatusInternalServerError, Err: err}
		}
		if !nf.Node.IsDirectory {
			continue
		}
		entry := fsListResponseEntry{Name: name, IsDir: true, Nlink: 2}
		if scope.AuthorizeFS(FSOpRead, childPath) == nil {
			entry.Mtime = nf.Node.CreatedAt.Unix()
			entry.Mode = nf.Mode
			entry.HasMode = nf.HasMode
			entry.ResourceID = nodeResourceID(nf)
		}
		out = append(out, entry)
	}
	return out, projectedDirectoryResult{Status: http.StatusOK}
}
