// Package service provides business logic services.
package service

import (
	"context"
	"time"

	"github.com/google/uuid"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/logging"
	"github.com/orchard9/rdev/internal/port"
)

// OperationService provides business logic for tracking operations.
// It wraps the repository with convenient methods for step-by-step tracking.
type OperationService struct {
	repo port.OperationRepository
}

// NewOperationService creates a new operation service.
func NewOperationService(repo port.OperationRepository) *OperationService {
	return &OperationService{
		repo: repo,
	}
}

// StartOperation creates a new operation and returns its ID.
// The operation starts in "running" status.
func (s *OperationService) StartOperation(
	ctx context.Context,
	projectID string,
	opType domain.OperationType,
	input map[string]any,
	requestID string,
) (string, error) {
	op := &domain.Operation{
		ID:        uuid.New().String(),
		ProjectID: projectID,
		Type:      opType,
		Status:    domain.OperationStatusRunning,
		RequestID: requestID,
		StartedAt: time.Now(),
		Input:     input,
		Steps:     []domain.OperationStep{},
	}

	log := logging.FromContext(ctx).WithService("operation")
	if err := s.repo.Create(ctx, op); err != nil {
		log.Error("failed to create operation",
			logging.FieldError, err,
			logging.FieldProjectID, projectID,
			"type", opType,
		)
		return "", err
	}

	log.Info("operation started",
		"operation_id", op.ID,
		logging.FieldProjectID, projectID,
		"type", opType,
	)

	return op.ID, nil
}

// StartStep adds a new step to an operation and marks it as running.
func (s *OperationService) StartStep(ctx context.Context, operationID, stepName string) error {
	step := domain.OperationStep{
		Name:      stepName,
		Status:    domain.OperationStatusRunning,
		StartedAt: time.Now(),
	}

	log := logging.FromContext(ctx).WithService("operation")
	if err := s.repo.AddStep(ctx, operationID, step); err != nil {
		log.Error("failed to start step",
			logging.FieldError, err,
			"operation_id", operationID,
			"step", stepName,
		)
		return err
	}

	return nil
}

// CompleteStep marks a step as completed with optional output.
func (s *OperationService) CompleteStep(
	ctx context.Context,
	operationID, stepName string,
	startedAt time.Time,
	output map[string]any,
) error {
	step := domain.OperationStep{
		Name:       stepName,
		Status:     domain.OperationStatusCompleted,
		StartedAt:  startedAt,
		DurationMs: time.Since(startedAt).Milliseconds(),
		Output:     output,
	}

	log := logging.FromContext(ctx).WithService("operation")
	if err := s.repo.UpdateStep(ctx, operationID, step); err != nil {
		log.Error("failed to complete step",
			logging.FieldError, err,
			"operation_id", operationID,
			"step", stepName,
		)
		return err
	}

	return nil
}

// FailStep marks a step as failed with error details.
func (s *OperationService) FailStep(
	ctx context.Context,
	operationID, stepName string,
	startedAt time.Time,
	errMsg, errDetail string,
) error {
	step := domain.OperationStep{
		Name:        stepName,
		Status:      domain.OperationStatusFailed,
		StartedAt:   startedAt,
		DurationMs:  time.Since(startedAt).Milliseconds(),
		Error:       errMsg,
		ErrorDetail: domain.TruncateErrorDetail(errDetail),
	}

	log := logging.FromContext(ctx).WithService("operation")
	if err := s.repo.UpdateStep(ctx, operationID, step); err != nil {
		log.Error("failed to fail step",
			logging.FieldError, err,
			"operation_id", operationID,
			"step", stepName,
		)
		return err
	}

	return nil
}

// CompleteOperation marks the operation as completed with optional output.
func (s *OperationService) CompleteOperation(
	ctx context.Context,
	operationID string,
	output map[string]any,
) error {
	log := logging.FromContext(ctx).WithService("operation")
	if err := s.repo.Complete(ctx, operationID, domain.OperationStatusCompleted, output, "", ""); err != nil {
		log.Error("failed to complete operation",
			logging.FieldError, err,
			"operation_id", operationID,
		)
		return err
	}

	log.Info("operation completed",
		"operation_id", operationID,
	)

	return nil
}

// FailOperation marks the operation as failed with error details.
func (s *OperationService) FailOperation(
	ctx context.Context,
	operationID string,
	errMsg, errDetail string,
) error {
	log := logging.FromContext(ctx).WithService("operation")
	if err := s.repo.Complete(ctx, operationID, domain.OperationStatusFailed, nil, errMsg, errDetail); err != nil {
		log.Error("failed to fail operation",
			logging.FieldError, err,
			"operation_id", operationID,
		)
		return err
	}

	log.Info("operation failed",
		"operation_id", operationID,
		logging.FieldError, errMsg,
	)

	return nil
}

// SetCommitSHA updates the commit SHA for an operation.
// Called after a git commit is created as part of the operation.
func (s *OperationService) SetCommitSHA(ctx context.Context, operationID, sha string) error {
	return s.repo.SetCommitSHA(ctx, operationID, sha)
}

// SetExternalRef updates the external reference for an operation.
// Called when linking to external systems like Woodpecker builds.
func (s *OperationService) SetExternalRef(ctx context.Context, operationID, ref string) error {
	op, err := s.repo.Get(ctx, operationID)
	if err != nil {
		return err
	}
	op.ExternalRef = ref
	return s.repo.Update(ctx, op)
}

// FindByCommit finds the operation that created a specific commit.
// Used to link builds to the operation that triggered them.
func (s *OperationService) FindByCommit(ctx context.Context, projectID, sha string) (*domain.Operation, error) {
	return s.repo.GetByCommitSHA(ctx, projectID, sha)
}

// Get retrieves an operation by ID.
func (s *OperationService) Get(ctx context.Context, operationID string) (*domain.Operation, error) {
	return s.repo.Get(ctx, operationID)
}

// List returns operations matching the filter criteria.
func (s *OperationService) List(ctx context.Context, filter domain.OperationFilters) ([]*domain.Operation, error) {
	return s.repo.List(ctx, filter)
}

// LinkToParent sets the triggered_by field to link to a parent operation.
func (s *OperationService) LinkToParent(ctx context.Context, operationID, parentID string) error {
	return s.repo.SetTriggeredBy(ctx, operationID, parentID)
}
