package content

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	service "github.com/tidbcloud/AgentTeam/internal/service/content"
)

const maxCommitBodyBytes = int64(24 << 20)

type Service interface {
	CreateContent(context.Context, service.CreateContentRequest) (service.Content, error)
	GetContent(context.Context, string, string) (service.Content, error)
	CommitVersion(context.Context, service.CommitVersionRequest) (service.CommitVersionResult, error)
	GetVersion(context.Context, string, string, string) (service.Version, error)
	ListVersions(context.Context, string, string, int) ([]service.Version, error)
	PathHistory(context.Context, string, string, string, int) ([]service.Path, error)
	Compare(context.Context, string, string, string, string) ([]service.CompareEntry, error)
	OpenFile(context.Context, string, string, string, string, *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error)
	OpenTreeArchive(context.Context, string, string, string, service.ArchiveLimits) (io.ReadCloser, service.ArchiveMeta, error)
	CreatePin(context.Context, service.CreatePinRequest) (service.Pin, error)
	ListPins(context.Context, string, string, int) ([]service.Pin, error)
	DeletePin(context.Context, string, string, string) error
}

type Handler struct{ service Service }

func New(service Service) *Handler { return &Handler{service: service} }

func (handler *Handler) ensureService() error {
	if handler == nil || handler.service == nil {
		return meshxapi.NewHTTPError(http.StatusServiceUnavailable, "Content Store is not configured")
	}
	return nil
}

// Create handles POST /api/v1/content-store/contents.
// It creates one inactive, workspace-scoped content lineage.
func (handler *Handler) Create(r meshxapi.AuthenticatedJSON[dto.ContentStoreCreateRequest]) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	content, err := handler.service.CreateContent(r.Context(), service.CreateContentRequest{WorkspaceID: r.WorkspaceID(), Domain: r.Body.Domain, BindingKey: r.Body.BindingKey, Model: service.TreeModel, Actor: r.Subject()})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(contentDTO(content)), nil
}

// Get handles GET /api/v1/content-store/contents/{contentId}.
// It returns lineage metadata without loading payload bytes.
func (handler *Handler) Get(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	content, err := handler.service.GetContent(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(contentDTO(content)), nil
}

// Commit handles POST /api/v1/content-store/contents/{contentId}/versions.
// It stages one bounded full payload and advances the head with CAS.
func (handler *Handler) Commit(r meshxapi.AuthenticatedJSON[dto.ContentStoreCommitRequest]) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	files, err := decodeFiles(r.Body.Files)
	if err != nil {
		return meshxapi.Response{}, meshxapi.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	result, err := handler.service.CommitVersion(r.Context(), service.CommitVersionRequest{
		WorkspaceID: r.WorkspaceID(), ContentID: r.Request.PathValue("contentId"), ExpectedHeadVersionID: r.Body.ExpectedHeadVersionID,
		ExpectedGeneration: r.Body.ExpectedGeneration, Actor: r.Subject(), Model: service.TreeModel, Files: files,
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(dto.ContentStoreCommitResponse{Content: contentDTO(result.Content), Version: versionDTO(result.Version)}), nil
}

// ListVersions handles GET /api/v1/content-store/contents/{contentId}/versions.
// It returns bounded version metadata only.
func (handler *Handler) ListVersions(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	limit, err := queryLimit(r.Request, 50, service.MaxVersionListRows)
	if err != nil {
		return meshxapi.Response{}, err
	}
	versions, err := handler.service.ListVersions(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"), limit)
	if err != nil {
		return meshxapi.Response{}, err
	}
	items := make([]dto.ContentStoreVersion, len(versions))
	for index := range versions {
		items[index] = versionDTO(versions[index])
	}
	return meshxapi.OK(dto.ContentStoreVersionList{Items: items}), nil
}

// GetVersion handles GET /api/v1/content-store/contents/{contentId}/versions/{versionId}.
// It returns metadata for one exact immutable version without following head.
func (handler *Handler) GetVersion(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	version, err := handler.service.GetVersion(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"), r.Request.PathValue("versionId"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.OK(versionDTO(version)), nil
}

// CreatePin handles POST /api/v1/content-store/contents/{contentId}/pins.
// It pins one exact immutable version under the lineage's configured pin cap.
func (handler *Handler) CreatePin(r meshxapi.AuthenticatedJSON[dto.ContentStorePinCreateRequest]) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	pin, err := handler.service.CreatePin(r.Context(), service.CreatePinRequest{
		WorkspaceID: r.WorkspaceID(), ContentID: r.Request.PathValue("contentId"),
		VersionID: r.Body.VersionID, Label: r.Body.Label, Actor: r.Subject(),
	})
	if err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.Created(pinDTO(pin)), nil
}

// ListPins handles GET /api/v1/content-store/contents/{contentId}/pins.
// It returns one bounded page of exact-version pins without loading payloads.
func (handler *Handler) ListPins(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	limit, err := queryLimit(r.Request, 50, service.MaxPinListRows)
	if err != nil {
		return meshxapi.Response{}, err
	}
	pins, err := handler.service.ListPins(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"), limit)
	if err != nil {
		return meshxapi.Response{}, err
	}
	items := make([]dto.ContentStorePin, len(pins))
	for index := range pins {
		items[index] = pinDTO(pins[index])
	}
	return meshxapi.OK(dto.ContentStorePinList{Items: items}), nil
}

// DeletePin handles DELETE /api/v1/content-store/contents/{contentId}/pins/{pinId}.
// It deletes one exact workspace/content pin and leaves the version immutable.
func (handler *Handler) DeletePin(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	if err := handler.service.DeletePin(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"), r.Request.PathValue("pinId")); err != nil {
		return meshxapi.Response{}, err
	}
	return meshxapi.NoContent(), nil
}

// PathHistory handles GET /api/v1/content-store/contents/{contentId}/path-history.
// It queries the transactional path/digest projection without payload reads.
func (handler *Handler) PathHistory(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	limit, err := queryLimit(r.Request, 50, service.MaxPathHistoryRows)
	if err != nil {
		return meshxapi.Response{}, err
	}
	rows, err := handler.service.PathHistory(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"), r.Request.URL.Query().Get("path"), limit)
	if err != nil {
		return meshxapi.Response{}, err
	}
	items := make([]dto.ContentStorePathHistoryItem, len(rows))
	for index, row := range rows {
		items[index] = dto.ContentStorePathHistoryItem{VersionID: row.VersionID, Path: row.Path, Digest: row.Digest, Kind: row.Kind, SizeBytes: row.SizeBytes, CreatedAt: row.CreatedAt.UTC().Format(timeLayout)}
	}
	return meshxapi.OK(dto.ContentStorePathHistory{Items: items}), nil
}

// Compare handles GET /api/v1/content-store/contents/{contentId}/compare.
// It compares bounded path summaries for two exact immutable versions.
func (handler *Handler) Compare(r meshxapi.AuthenticatedRequest) (meshxapi.Response, error) {
	if err := handler.ensureService(); err != nil {
		return meshxapi.Response{}, err
	}
	query := r.Request.URL.Query()
	items, err := handler.service.Compare(r.Context(), r.WorkspaceID(), r.Request.PathValue("contentId"), query.Get("before"), query.Get("after"))
	if err != nil {
		return meshxapi.Response{}, err
	}
	result := make([]dto.ContentStoreCompareEntry, len(items))
	for index, item := range items {
		result[index] = dto.ContentStoreCompareEntry{Path: item.Path, Change: item.Change, BeforeDigest: item.BeforeDigest, AfterDigest: item.AfterDigest, BeforeBytes: item.BeforeBytes, AfterBytes: item.AfterBytes}
	}
	return meshxapi.OK(dto.ContentStoreCompare{Items: result}), nil
}

// OpenFile handles GET /api/v1/content-store/contents/{contentId}/versions/{versionId}/files/{path...}.
// It streams one exact immutable file and never materializes the full version.
func (handler *Handler) OpenFile(w http.ResponseWriter, request *http.Request) {
	if err := handler.ensureService(); err != nil {
		meshxapi.WriteError(w, err)
		return
	}
	r, err := meshxapi.NewAuthenticatedRequest(request)
	if err != nil {
		meshxapi.WriteError(w, err)
		return
	}
	byteRange, err := requestedRange(request)
	if err != nil {
		meshxapi.WriteError(w, err)
		return
	}
	body, meta, err := handler.service.OpenFile(r.Context(), r.WorkspaceID(), request.PathValue("contentId"), request.PathValue("versionId"), request.PathValue("path"), byteRange)
	if err != nil {
		writeStreamError(w, err)
		return
	}
	defer body.Close()
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("ETag", `"sha256:`+string(meta.Digest)+`"`)
	if byteRange == nil {
		w.Header().Set("Content-Length", strconv.FormatInt(meta.SizeBytes, 10))
	} else {
		w.Header().Set("Content-Length", strconv.FormatInt(byteRange.Length, 10))
		w.WriteHeader(http.StatusPartialContent)
	}
	_, _ = io.Copy(w, body)
}

// OpenArchive handles GET /api/v1/content-store/contents/{contentId}/versions/{versionId}/archive.
// It streams one exact bounded tree version as a ZIP without materializing it.
func (handler *Handler) OpenArchive(w http.ResponseWriter, request *http.Request) {
	if err := handler.ensureService(); err != nil {
		meshxapi.WriteError(w, err)
		return
	}
	r, err := meshxapi.NewAuthenticatedRequest(request)
	if err != nil {
		meshxapi.WriteError(w, err)
		return
	}
	body, _, err := handler.service.OpenTreeArchive(r.Context(), r.WorkspaceID(), request.PathValue("contentId"), request.PathValue("versionId"), service.ArchiveLimits{
		MaxFiles: contentpayload.DefaultLimits.MaxFiles, MaxFileBytes: contentpayload.DefaultLimits.MaxFileBytes, MaxTotalBytes: contentpayload.DefaultLimits.MaxTotalBytes,
	})
	if err != nil {
		writeStreamError(w, err)
		return
	}
	defer body.Close()
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", `attachment; filename="content-version.zip"`)
	_, _ = io.Copy(w, body)
}

func requestedRange(request *http.Request) (*contentpayload.ByteRange, error) {
	query := request.URL.Query()
	if query.Get("offset") == "" && query.Get("length") == "" {
		return nil, nil
	}
	offset, err := strconv.ParseInt(query.Get("offset"), 10, 64)
	if err != nil || offset < 0 {
		return nil, meshxapi.NewHTTPError(http.StatusBadRequest, "offset must be a non-negative integer")
	}
	length, err := strconv.ParseInt(query.Get("length"), 10, 64)
	if err != nil || length < 0 || length > contentpayload.DefaultLimits.MaxFileBytes {
		return nil, meshxapi.NewHTTPError(http.StatusBadRequest, "length is invalid")
	}
	return &contentpayload.ByteRange{Offset: offset, Length: length}, nil
}

func writeStreamError(w http.ResponseWriter, err error) {
	switch {
	case service.IsNotFound(err), errors.Is(err, contentpayload.ErrNotFound), errors.Is(err, service.ErrAccessDenied):
		meshxapi.WriteError(w, meshxapi.NewHTTPError(http.StatusNotFound, "Content Store object not found"))
	case service.IsConflict(err):
		meshxapi.WriteError(w, meshxapi.NewHTTPError(http.StatusConflict, err.Error()))
	default:
		meshxapi.WriteError(w, err)
	}
}

func decodeFiles(inputs []dto.ContentStoreFileInput) ([]contentpayload.File, error) {
	if len(inputs) > contentpayload.DefaultLimits.MaxFiles {
		return nil, fmt.Errorf("files exceed %d entry limit", contentpayload.DefaultLimits.MaxFiles)
	}
	files := make([]contentpayload.File, 0, len(inputs))
	var total int64
	for _, input := range inputs {
		decoded, err := base64.StdEncoding.Strict().DecodeString(input.ContentBase64)
		if err != nil {
			return nil, fmt.Errorf("file %q contentBase64 is invalid", input.Path)
		}
		total += int64(len(decoded))
		if total > maxCommitBodyBytes {
			return nil, fmt.Errorf("decoded files exceed %d byte API limit", maxCommitBodyBytes)
		}
		files = append(files, contentpayload.File{Path: input.Path, Size: int64(len(decoded)), Executable: input.Executable, Content: bytes.NewReader(decoded)})
	}
	return files, nil
}

func queryLimit(request *http.Request, fallback, maximum int) (int, error) {
	raw := strings.TrimSpace(request.URL.Query().Get("limit"))
	if raw == "" {
		return fallback, nil
	}
	limit, err := strconv.Atoi(raw)
	if err != nil || limit <= 0 || limit > maximum {
		return 0, meshxapi.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("limit must be between 1 and %d", maximum))
	}
	return limit, nil
}

func contentDTO(content service.Content) dto.ContentStoreContent {
	return dto.ContentStoreContent{ID: content.ID, WorkspaceID: content.WorkspaceID, Domain: content.Domain, BindingKey: content.BindingKey, HeadVersionID: content.HeadVersionID, Revision: content.Revision, Generation: content.Generation, RetainedVersionCount: content.RetainedVersionCount, StoredPayloadBytes: content.StoredPayloadBytes, MaxRetainedVersions: content.MaxRetainedVersions, MaxStoredPayloadBytes: content.MaxStoredPayloadBytes, MaxPins: content.MaxPins, CreatedBy: content.CreatedBy, UpdatedBy: content.UpdatedBy, CreatedAt: content.CreatedAt.UTC().Format(timeLayout), UpdatedAt: content.UpdatedAt.UTC().Format(timeLayout)}
}

func versionDTO(version service.Version) dto.ContentStoreVersion {
	return dto.ContentStoreVersion{ID: version.ID, ContentID: version.ContentID, ParentVersionID: version.ParentVersionID, Generation: version.Generation, RootDigest: version.RootDigest, SizeBytes: version.SizeBytes, FileCount: version.FileCount, SummaryBytes: version.SummaryBytes, CreatedBy: version.CreatedBy, CreatedAt: version.CreatedAt.UTC().Format(timeLayout)}
}

func pinDTO(pin service.Pin) dto.ContentStorePin {
	return dto.ContentStorePin{ID: pin.ID, ContentID: pin.ContentID, VersionID: pin.VersionID, Label: pin.Label, CreatedBy: pin.CreatedBy, CreatedAt: pin.CreatedAt.UTC().Format(timeLayout)}
}

func errorHandling() meshxapi.HandlerOption {
	return meshxapi.WithErrorClassifier(func(err error) (meshxapi.ErrorResponse, bool) {
		switch {
		case service.IsNotFound(err), errors.Is(err, contentpayload.ErrNotFound), errors.Is(err, service.ErrAccessDenied):
			return meshxapi.ErrorResponse{Status: http.StatusNotFound, Message: "Content Store object not found", Code: "content_store.not_found"}, true
		case errors.Is(err, service.ErrPolicyUnavailable):
			return meshxapi.ErrorResponse{Status: http.StatusServiceUnavailable, Message: "Content access policy is unavailable", Code: "content_store.policy_unavailable"}, true
		case service.IsConflict(err):
			return meshxapi.ErrorResponse{Status: http.StatusConflict, Message: err.Error(), Code: "content_store.conflict"}, true
		case service.IsQuotaExceeded(err):
			return meshxapi.ErrorResponse{Status: http.StatusInsufficientStorage, Message: err.Error(), Code: "content_store.quota_exceeded"}, true
		default:
			return meshxapi.ErrorResponse{}, false
		}
	})
}

const timeLayout = "2006-01-02T15:04:05.000Z07:00"
