package automation

import (
	"bytes"
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"math"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/automation/blob"
	automationstore "github.com/tidbcloud/AgentTeam/internal/store/automation"
	"gorm.io/gorm"
)

const (
	automationLogStreamMaxLength    = 32
	automationArtifactNameMaxLength = 255
)

type AutomationLogChunkContent struct {
	Chunk   automationstore.AutomationLogChunk
	Content string
}

type AutomationArtifactContent struct {
	Artifact automationstore.AutomationArtifact
	Content  []byte
}

const automationBlobOrphanSweepGrace = 15 * time.Minute

type AutomationBlobLifecycleResult struct {
	ExpiredArtifactsScanned int64
	ExpiredArtifactsDeleted int64
	OrphanObjectsScanned    int64
	OrphanObjectsDeleted    int64
}

type AutomationBlobMissingObject struct {
	Kind        string
	WorkspaceID string
	RunID       string
	ID          string
	ObjectKey   string
}

type AutomationBlobHealthCheck struct {
	Checked int64
	Missing []AutomationBlobMissingObject
}

func (s *AutomationsService) ListAutomationLogChunks(ctx context.Context, workspaceID string, subject string, runID string, jobRunID string, stream string, page int, perPage int) ([]automationstore.AutomationLogChunk, error) {
	if _, err := s.GetAutomationRun(ctx, workspaceID, subject, runID); err != nil {
		return nil, err
	}
	page, perPage = normalizeAutomationPageWithMax(page, perPage, 100)
	return s.store.ListLogChunks(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(runID), strings.TrimSpace(jobRunID), strings.TrimSpace(stream), page, perPage)
}

func (s *AutomationsService) ReadAutomationLogChunkContent(ctx context.Context, workspaceID string, subject string, runID string, jobRunID string, stream string, sequence int64) (AutomationLogChunkContent, error) {
	run, err := s.GetAutomationRun(ctx, workspaceID, subject, runID)
	if err != nil {
		return AutomationLogChunkContent{}, err
	}
	jobRunID = strings.TrimSpace(jobRunID)
	stream = normalizeAutomationLogStream(stream)
	if err := s.automationAuthorizer().CanReadRun(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{Run: run, JobRunID: jobRunID, LogStream: stream, LogSequence: sequence})); err != nil {
		return AutomationLogChunkContent{}, err
	}
	chunk, err := s.store.GetLogChunk(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(runID), jobRunID, stream, sequence)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return AutomationLogChunkContent{}, notFound("Automation log chunk not found")
	}
	if err != nil {
		return AutomationLogChunkContent{}, err
	}
	payload, err := s.readAutomationBlob(ctx, chunk.ObjectKey, chunk.SizeBytes, chunk.SHA256)
	if err != nil {
		return AutomationLogChunkContent{}, err
	}
	return AutomationLogChunkContent{Chunk: chunk, Content: string(payload)}, nil
}

func (s *AutomationsService) ListAutomationArtifacts(ctx context.Context, workspaceID string, subject string, runID string, jobRunID string, page int, perPage int) ([]automationstore.AutomationArtifact, error) {
	if _, err := s.GetAutomationRun(ctx, workspaceID, subject, runID); err != nil {
		return nil, err
	}
	page, perPage = normalizeAutomationPageWithMax(page, perPage, 100)
	return s.store.ListArtifacts(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(runID), strings.TrimSpace(jobRunID), page, perPage)
}

func (s *AutomationsService) ReadAutomationArtifactContent(ctx context.Context, workspaceID string, subject string, runID string, artifactID string) (AutomationArtifactContent, error) {
	run, err := s.GetAutomationRun(ctx, workspaceID, subject, runID)
	if err != nil {
		return AutomationArtifactContent{}, err
	}
	artifactID = strings.TrimSpace(artifactID)
	if err := s.automationAuthorizer().CanReadRun(ctx, s.automationAuthorizationRequest(ctx, workspaceID, subject, AutomationAuthorizationRequest{Run: run, ArtifactID: artifactID})); err != nil {
		return AutomationArtifactContent{}, err
	}
	artifact, err := s.store.GetArtifact(ctx, strings.TrimSpace(workspaceID), strings.TrimSpace(runID), artifactID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return AutomationArtifactContent{}, notFound("Automation artifact not found")
	}
	if err != nil {
		return AutomationArtifactContent{}, err
	}
	payload, err := s.readAutomationBlob(ctx, artifact.ObjectKey, artifact.SizeBytes, artifact.SHA256)
	if err != nil {
		return AutomationArtifactContent{}, err
	}
	return AutomationArtifactContent{Artifact: artifact, Content: payload}, nil
}

func (s *AutomationsService) ensureAutomationBlobStore() (blob.Store, error) {
	if s == nil || s.blobStore == nil {
		return nil, fmt.Errorf("automation blob store is not configured")
	}
	return s.blobStore, nil
}

func (s *AutomationsService) SweepExpiredAutomationArtifacts(ctx context.Context, limit int) (AutomationBlobLifecycleResult, error) {
	if err := s.ensureStore(); err != nil {
		return AutomationBlobLifecycleResult{}, err
	}
	blobStore, err := s.ensureAutomationBlobStore()
	if err != nil {
		return AutomationBlobLifecycleResult{}, err
	}
	artifacts, err := s.store.ListExpiredArtifacts(ctx, s.now(), limit)
	if err != nil {
		return AutomationBlobLifecycleResult{}, err
	}
	var result AutomationBlobLifecycleResult
	result.ExpiredArtifactsScanned = int64(len(artifacts))
	for _, artifact := range artifacts {
		if strings.TrimSpace(artifact.ObjectKey) != "" {
			if err := blobStore.Delete(ctx, artifact.ObjectKey); err != nil && !errors.Is(err, blob.ErrNotFound) {
				return result, err
			}
		}
		deleted, err := s.store.DeleteArtifactMetadata(ctx, artifact.WorkspaceID, artifact.RunID, artifact.ID)
		if err != nil {
			return result, err
		}
		if deleted {
			result.ExpiredArtifactsDeleted++
		}
	}
	return result, nil
}

func (s *AutomationsService) SweepAutomationOrphanBlobs(ctx context.Context, prefix string, limit int) (AutomationBlobLifecycleResult, error) {
	if err := s.ensureStore(); err != nil {
		return AutomationBlobLifecycleResult{}, err
	}
	blobStore, err := s.ensureAutomationBlobStore()
	if err != nil {
		return AutomationBlobLifecycleResult{}, err
	}
	lister, ok := blobStore.(blob.Lister)
	if !ok {
		return AutomationBlobLifecycleResult{}, fmt.Errorf("automation blob store does not support object listing")
	}
	if strings.TrimSpace(prefix) == "" {
		prefix = "workspaces"
	}
	objects, err := lister.List(ctx, prefix, limit)
	if err != nil {
		return AutomationBlobLifecycleResult{}, err
	}
	var result AutomationBlobLifecycleResult
	result.OrphanObjectsScanned = int64(len(objects))
	orphanCutoff := s.now().Add(-automationBlobOrphanSweepGrace)
	oldObjects := make([]blob.Object, 0, len(objects))
	objectKeys := make([]string, 0, len(objects))
	for _, object := range objects {
		if object.ModTime.IsZero() || object.ModTime.After(orphanCutoff) {
			continue
		}
		key := strings.TrimSpace(object.Key)
		if key == "" {
			continue
		}
		object.Key = key
		oldObjects = append(oldObjects, object)
		objectKeys = append(objectKeys, key)
	}
	existingKeys, err := s.store.ExistingAutomationBlobObjectKeys(ctx, objectKeys)
	if err != nil {
		return result, err
	}
	for _, object := range oldObjects {
		if _, exists := existingKeys[object.Key]; exists {
			continue
		}
		if err := blobStore.Delete(ctx, object.Key); err != nil && !errors.Is(err, blob.ErrNotFound) {
			return result, err
		}
		result.OrphanObjectsDeleted++
	}
	return result, nil
}

func (s *AutomationsService) CheckAutomationBlobObjects(ctx context.Context, limit int) (AutomationBlobHealthCheck, error) {
	if err := s.ensureStore(); err != nil {
		return AutomationBlobHealthCheck{}, err
	}
	blobStore, err := s.ensureAutomationBlobStore()
	if err != nil {
		return AutomationBlobHealthCheck{}, err
	}
	refs, err := s.store.ListAutomationBlobReferences(ctx, limit)
	if err != nil {
		return AutomationBlobHealthCheck{}, err
	}
	result := AutomationBlobHealthCheck{Checked: int64(len(refs))}
	for _, ref := range refs {
		if strings.TrimSpace(ref.ObjectKey) == "" {
			result.Missing = append(result.Missing, AutomationBlobMissingObject{Kind: ref.Kind, WorkspaceID: ref.WorkspaceID, RunID: ref.RunID, ID: ref.ID, ObjectKey: ref.ObjectKey})
			continue
		}
		body, err := blobStore.Get(ctx, ref.ObjectKey)
		if errors.Is(err, blob.ErrNotFound) {
			result.Missing = append(result.Missing, AutomationBlobMissingObject{Kind: ref.Kind, WorkspaceID: ref.WorkspaceID, RunID: ref.RunID, ID: ref.ID, ObjectKey: ref.ObjectKey})
			continue
		}
		if err != nil {
			return result, err
		}
		if body != nil {
			if err := body.Close(); err != nil {
				return result, fmt.Errorf("close automation blob object: %w", err)
			}
		}
	}
	return result, nil
}

func (s *AutomationsService) putAutomationBlob(ctx context.Context, key string, payload []byte) error {
	blobStore, err := s.ensureAutomationBlobStore()
	if err != nil {
		return err
	}
	return blobStore.Put(ctx, key, bytes.NewReader(payload), int64(len(payload)))
}

func (s *AutomationsService) deleteAutomationBlob(ctx context.Context, key string) {
	blobStore, err := s.ensureAutomationBlobStore()
	if err != nil {
		return
	}
	_ = blobStore.Delete(ctx, key)
}

func (s *AutomationsService) deleteAutomationLogBlobIfMetadataMismatch(ctx context.Context, chunk automationstore.AutomationLogChunk) {
	if s == nil || s.store == nil {
		return
	}
	stored, err := s.store.GetLogChunk(ctx, chunk.WorkspaceID, chunk.RunID, chunk.JobRunID, chunk.Stream, chunk.Sequence)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		s.deleteAutomationBlob(ctx, chunk.ObjectKey)
		return
	}
	if err != nil {
		return
	}
	if _, err := s.readAutomationBlob(ctx, stored.ObjectKey, stored.SizeBytes, stored.SHA256); err != nil {
		s.deleteAutomationBlob(ctx, chunk.ObjectKey)
	}
}

func (s *AutomationsService) readAutomationBlob(ctx context.Context, key string, size int64, sha256Hex string) ([]byte, error) {
	blobStore, err := s.ensureAutomationBlobStore()
	if err != nil {
		return nil, err
	}
	body, err := blobStore.Get(ctx, key)
	if errors.Is(err, blob.ErrNotFound) {
		return nil, notFound("Automation blob object is missing")
	}
	if err != nil {
		return nil, err
	}
	defer body.Close()
	payload, err := io.ReadAll(io.LimitReader(body, size+1))
	if err != nil {
		return nil, fmt.Errorf("read automation blob object: %w", err)
	}
	if int64(len(payload)) != size {
		return nil, fmt.Errorf("automation blob object size mismatch: got %d bytes, want %d", len(payload), size)
	}
	if blob.SHA256Hex(payload) != strings.ToLower(strings.TrimSpace(sha256Hex)) {
		return nil, fmt.Errorf("automation blob object checksum mismatch")
	}
	return payload, nil
}

func automationBase64EncodedLen(decodedBytes int64) int64 {
	if decodedBytes < 0 {
		return 0
	}
	if decodedBytes > (math.MaxInt64/4)*3-2 {
		return math.MaxInt64
	}
	return ((decodedBytes + 2) / 3) * 4
}

func validateAutomationBlobSHA256(expected string, actual string, required bool, message string) error {
	expected = strings.ToLower(strings.TrimSpace(expected))
	if expected == "" {
		if required {
			return invalidArgument(message)
		}
		return nil
	}
	if len(expected) != 64 {
		return invalidArgument(message)
	}
	if _, err := hex.DecodeString(expected); err != nil {
		return invalidArgument(message)
	}
	if expected != strings.ToLower(strings.TrimSpace(actual)) {
		return invalidArgument(message)
	}
	return nil
}

func normalizeAutomationLogStream(stream string) string {
	trimmed := strings.ToLower(strings.TrimSpace(stream))
	if trimmed == "" {
		return "stdout"
	}
	return trimmed
}

func automationBlobContentType(contentType string, fallback string) (string, error) {
	trimmed := strings.TrimSpace(contentType)
	if trimmed == "" {
		trimmed = fallback
	}
	if len(trimmed) > 128 {
		return "", invalidArgument("Automation blob content_type exceeds maximum length")
	}
	return trimmed, nil
}

func automationLogChunkID(jobRunID string, stream string, sequence int64) string {
	return blob.LogChunkID(jobRunID, stream, sequence)
}

func automationRunnerLogEventID(jobRunID string, stream string, sequence int64) string {
	return blob.LogEventID(jobRunID, stream, sequence)
}

func automationRunnerArtifactEventID(artifactID string) string {
	return blob.ArtifactEventID(artifactID)
}

func automationLogChunkObjectKey(workspaceID string, runID string, jobRunID string, stream string, sequence int64) (string, error) {
	if sequence <= 0 {
		return "", invalidArgument("Automation runner log sequence is required")
	}
	key, err := blob.LogChunkObjectKey(workspaceID, runID, jobRunID, stream, sequence)
	if err != nil {
		return "", invalidArgument(err.Error())
	}
	return key, nil
}

func automationArtifactObjectKey(workspaceID string, runID string, jobRunID string, artifactID string, name string) (string, error) {
	key, err := blob.ArtifactObjectKey(workspaceID, runID, jobRunID, artifactID, name)
	if err != nil {
		return "", invalidArgument(err.Error())
	}
	return key, nil
}

func parseAutomationExpiresAt(raw string) (*time.Time, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	parsed, err := time.Parse(time.RFC3339Nano, raw)
	if err != nil {
		return nil, invalidArgument("Automation artifact expires_at is invalid")
	}
	utc := parsed.UTC()
	return &utc, nil
}

func mapAutomationBlobMetadataError(err error) error {
	if errors.Is(err, automationstore.ErrAutomationBlobMetadataConflict) {
		return invalidArgument("Automation log chunk sequence already exists with different content")
	}
	return err
}
