// Package service provides business logic services.
package service

import (
	"context"
	"fmt"
	"net/url"
	"strings"
	"time"

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

// CheckoutService orchestrates checkout/checkin operations.
// It manages temporary git access tokens for local development.
type CheckoutService struct {
	checkoutRepo port.CheckoutRepository
	gitRepo      port.GitRepository
	workQueue    port.WorkQueue
	projectRepo  port.ProjectRepository

	// giteaURL is the base Gitea URL for building clone URLs.
	giteaURL string

	// defaultOwner is the default git org/user for repositories.
	defaultOwner string

	// defaultExpiry is the default checkout token expiry duration.
	defaultExpiry time.Duration
}

// CheckoutServiceConfig holds configuration for the checkout service.
type CheckoutServiceConfig struct {
	GiteaURL      string
	DefaultOwner  string
	DefaultExpiry time.Duration
}

// NewCheckoutService creates a new checkout service.
func NewCheckoutService(
	checkoutRepo port.CheckoutRepository,
	gitRepo port.GitRepository,
	projectRepo port.ProjectRepository,
	cfg CheckoutServiceConfig,
) *CheckoutService {
	expiry := cfg.DefaultExpiry
	if expiry == 0 {
		expiry = 24 * time.Hour
	}
	return &CheckoutService{
		checkoutRepo:  checkoutRepo,
		gitRepo:       gitRepo,
		projectRepo:   projectRepo,
		giteaURL:      cfg.GiteaURL,
		defaultOwner:  cfg.DefaultOwner,
		defaultExpiry: expiry,
	}
}

// WithWorkQueue adds a work queue for review task enqueueing.
func (s *CheckoutService) WithWorkQueue(queue port.WorkQueue) *CheckoutService {
	s.workQueue = queue
	return s
}

// CheckoutRequest contains parameters for checking out a project.
type CheckoutRequest struct {
	// ProjectID is the project to checkout.
	ProjectID domain.ProjectID

	// Branch is the branch to checkout. If empty and NewBranch is set, creates NewBranch from main.
	Branch string

	// NewBranch is an optional new branch name to create.
	NewBranch string

	// FromRef is the reference to create the new branch from (default: "main").
	FromRef string

	// FeatureSlug is an optional SDLC feature to link.
	FeatureSlug string

	// ExpiresIn is the optional token expiry duration (default: 24h).
	ExpiresIn time.Duration

	// CheckedOutBy is the user/key creating the checkout.
	CheckedOutBy string
}

// CheckoutResult contains the result of a checkout operation.
type CheckoutResult struct {
	// Checkout is the created checkout record.
	Checkout *domain.Checkout

	// AuthenticatedCloneURL is the clone URL with embedded token.
	// This is only available at creation time and should not be stored.
	AuthenticatedCloneURL string

	// Instructions are human-readable instructions for using the checkout.
	Instructions string
}

// Checkout creates a new checkout session with a temporary git token.
func (s *CheckoutService) Checkout(ctx context.Context, req CheckoutRequest) (*CheckoutResult, error) {
	log := logging.FromContext(ctx).WithService("CheckoutService")

	// Validate project exists
	project, err := s.projectRepo.Get(ctx, req.ProjectID)
	if err != nil {
		return nil, fmt.Errorf("get project: %w", err)
	}

	// Determine branch
	branch := req.Branch
	if req.NewBranch != "" {
		branch = req.NewBranch
	}
	if branch == "" {
		return nil, fmt.Errorf("branch or new_branch is required")
	}

	// Get repo info (owner/name from project)
	repoOwner := s.defaultOwner
	repoName := string(project.ID)

	// Check if branch exists or needs to be created
	if req.NewBranch != "" {
		fromRef := req.FromRef
		if fromRef == "" {
			fromRef = "main"
		}

		log.Info("creating new branch",
			logging.FieldProjectID, req.ProjectID,
			"branch", req.NewBranch,
			"from_ref", fromRef,
		)

		_, err := s.gitRepo.CreateBranch(ctx, repoOwner, repoName, req.NewBranch, fromRef)
		if err != nil {
			return nil, fmt.Errorf("create branch: %w", err)
		}
	} else {
		// Verify branch exists
		branches, err := s.gitRepo.ListBranches(ctx, repoOwner, repoName)
		if err != nil {
			return nil, fmt.Errorf("list branches: %w", err)
		}

		found := false
		for _, b := range branches {
			if b.Name == branch {
				found = true
				if b.Protected {
					return nil, domain.ErrBranchProtected
				}
				break
			}
		}
		if !found {
			return nil, domain.ErrBranchNotFound
		}
	}

	// Check for existing active checkout on this branch
	existing, err := s.checkoutRepo.GetByProjectBranch(ctx, req.ProjectID, branch)
	if err == nil && existing != nil {
		// Check if expired (cleanup job may not have run)
		if existing.IsExpired() {
			// Mark as expired and revoke token
			_ = s.checkoutRepo.UpdateStatus(ctx, existing.ID, domain.CheckoutStatusExpired)
			_ = s.gitRepo.DeleteAccessToken(ctx, existing.GiteaTokenID)
		} else {
			return nil, domain.ErrCheckoutAlreadyExists
		}
	}

	// Calculate expiry
	expiry := s.defaultExpiry
	if req.ExpiresIn > 0 {
		expiry = req.ExpiresIn
	}
	expiresAt := time.Now().Add(expiry)

	// Create Gitea access token
	tokenName := fmt.Sprintf("checkout-%s-%s-%d", repoName, branch, time.Now().Unix())
	token, err := s.gitRepo.CreateAccessToken(ctx, tokenName, []string{"write:repository"}, &expiresAt)
	if err != nil {
		return nil, fmt.Errorf("create access token: %w", err)
	}

	// Build clone URLs (authenticated for return, base for storage)
	authCloneURL, baseCloneURL, err := s.buildCloneURLs(token.Token, repoOwner, repoName)
	if err != nil {
		// Clean up token on error
		_ = s.gitRepo.DeleteAccessToken(ctx, token.ID)
		return nil, fmt.Errorf("build clone url: %w", err)
	}

	// Create checkout record (stores base URL without token)
	checkout := &domain.Checkout{
		ProjectID:      req.ProjectID,
		Branch:         branch,
		FeatureSlug:    req.FeatureSlug,
		GiteaTokenID:   token.ID,
		GiteaTokenName: token.Name,
		CloneURL:       baseCloneURL, // Base URL only - no token
		CheckedOutBy:   req.CheckedOutBy,
		CheckedOutAt:   time.Now(),
		ExpiresAt:      expiresAt,
		Status:         domain.CheckoutStatusActive,
	}

	if err := s.checkoutRepo.Create(ctx, checkout); err != nil {
		// Clean up token on error
		_ = s.gitRepo.DeleteAccessToken(ctx, token.ID)
		return nil, fmt.Errorf("create checkout record: %w", err)
	}

	log.Info("checkout created",
		logging.FieldProjectID, req.ProjectID,
		"checkout_id", checkout.ID,
		"branch", branch,
		"expires_at", expiresAt,
	)

	instructions := fmt.Sprintf(`Clone with:
  git clone %s
  cd %s
  git checkout %s

Work locally, then push and call checkin when ready.
Token expires: %s`, authCloneURL, repoName, branch, expiresAt.Format(time.RFC3339))

	return &CheckoutResult{
		Checkout:              checkout,
		AuthenticatedCloneURL: authCloneURL, // Only returned once at creation
		Instructions:          instructions,
	}, nil
}

// CheckinRequest contains parameters for checking in.
type CheckinRequest struct {
	// CheckoutID is the checkout to complete.
	CheckoutID domain.CheckoutID

	// SkipReview skips the automatic review (merge directly if desired).
	SkipReview bool

	// AutoMerge attempts to merge to main if review passes.
	AutoMerge bool
}

// CheckinResult contains the result of a checkin operation.
type CheckinResult struct {
	// CheckoutID is the completed checkout ID.
	CheckoutID domain.CheckoutID

	// ReviewTaskID is the ID of the queued review task (if review requested).
	ReviewTaskID string

	// Status is the current checkout status.
	Status domain.CheckoutStatus
}

// Checkin completes a checkout and optionally queues a review.
func (s *CheckoutService) Checkin(ctx context.Context, req CheckinRequest) (*CheckinResult, error) {
	log := logging.FromContext(ctx).WithService("CheckoutService")

	// Get checkout
	checkout, err := s.checkoutRepo.Get(ctx, req.CheckoutID)
	if err != nil {
		return nil, err
	}

	// Verify active
	if checkout.Status != domain.CheckoutStatusActive {
		return nil, domain.ErrCheckoutNotActive
	}

	// Revoke token immediately (security: token should not be usable after checkin)
	if err := s.gitRepo.DeleteAccessToken(ctx, checkout.GiteaTokenID); err != nil {
		log.Warn("failed to revoke checkout token",
			"checkout_id", checkout.ID,
			"token_id", checkout.GiteaTokenID,
			logging.FieldError, err,
		)
		// Continue anyway - token revocation failure shouldn't block checkin
	}

	var reviewTaskID string

	// Queue review task if requested
	if !req.SkipReview && s.workQueue != nil {
		task := &domain.WorkTask{
			ProjectID: string(checkout.ProjectID),
			Type:      domain.WorkTaskType("review_checkout"),
			Spec: map[string]any{
				"checkout_id":  string(checkout.ID),
				"branch":       checkout.Branch,
				"feature_slug": checkout.FeatureSlug,
				"auto_merge":   req.AutoMerge,
			},
			Priority:   5, // Normal priority
			MaxRetries: 1, // Reviews shouldn't retry automatically
		}

		taskID, err := s.workQueue.Enqueue(ctx, task)
		if err != nil {
			log.Warn("failed to enqueue review task",
				"checkout_id", checkout.ID,
				logging.FieldError, err,
			)
		} else {
			reviewTaskID = taskID
			log.Info("review task queued",
				"checkout_id", checkout.ID,
				"task_id", taskID,
			)
		}
	}

	// Mark checked in
	if err := s.checkoutRepo.SetCheckedIn(ctx, checkout.ID, reviewTaskID); err != nil {
		return nil, fmt.Errorf("set checked in: %w", err)
	}

	log.Info("checkout completed",
		logging.FieldProjectID, checkout.ProjectID,
		"checkout_id", checkout.ID,
		"branch", checkout.Branch,
		"review_queued", reviewTaskID != "",
	)

	return &CheckinResult{
		CheckoutID:   checkout.ID,
		ReviewTaskID: reviewTaskID,
		Status:       domain.CheckoutStatusCheckedIn,
	}, nil
}

// ListBranches returns all branches for a project's repository.
func (s *CheckoutService) ListBranches(ctx context.Context, projectID domain.ProjectID) ([]*domain.GitBranch, error) {
	// Get project to verify it exists
	project, err := s.projectRepo.Get(ctx, projectID)
	if err != nil {
		return nil, fmt.Errorf("get project: %w", err)
	}

	repoOwner := s.defaultOwner
	repoName := string(project.ID)

	return s.gitRepo.ListBranches(ctx, repoOwner, repoName)
}

// Get retrieves a checkout by ID.
func (s *CheckoutService) Get(ctx context.Context, id domain.CheckoutID) (*domain.Checkout, error) {
	return s.checkoutRepo.Get(ctx, id)
}

// List returns checkouts for a project.
func (s *CheckoutService) List(ctx context.Context, projectID domain.ProjectID) ([]*domain.Checkout, error) {
	return s.checkoutRepo.ListByProject(ctx, projectID)
}

// Revoke manually revokes an active checkout.
func (s *CheckoutService) Revoke(ctx context.Context, id domain.CheckoutID) error {
	log := logging.FromContext(ctx).WithService("CheckoutService")

	checkout, err := s.checkoutRepo.Get(ctx, id)
	if err != nil {
		return err
	}

	if checkout.Status != domain.CheckoutStatusActive {
		return domain.ErrCheckoutNotActive
	}

	// Revoke token
	if err := s.gitRepo.DeleteAccessToken(ctx, checkout.GiteaTokenID); err != nil {
		log.Warn("failed to revoke checkout token",
			"checkout_id", checkout.ID,
			"token_id", checkout.GiteaTokenID,
			logging.FieldError, err,
		)
	}

	// Update status
	if err := s.checkoutRepo.UpdateStatus(ctx, id, domain.CheckoutStatusRevoked); err != nil {
		return fmt.Errorf("update status: %w", err)
	}

	log.Info("checkout revoked",
		"checkout_id", id,
		logging.FieldProjectID, checkout.ProjectID,
	)

	return nil
}

// CleanupExpired marks expired checkouts and revokes their tokens.
// Returns the number of checkouts cleaned up.
func (s *CheckoutService) CleanupExpired(ctx context.Context) (int, error) {
	log := logging.FromContext(ctx).WithService("CheckoutService")

	tokenIDs, err := s.checkoutRepo.CleanupExpired(ctx)
	if err != nil {
		return 0, fmt.Errorf("cleanup expired: %w", err)
	}

	// Revoke tokens
	for _, tokenID := range tokenIDs {
		if err := s.gitRepo.DeleteAccessToken(ctx, tokenID); err != nil {
			log.Warn("failed to revoke expired token",
				"token_id", tokenID,
				logging.FieldError, err,
			)
		}
	}

	if len(tokenIDs) > 0 {
		log.Info("cleaned up expired checkouts", "count", len(tokenIDs))
	}

	return len(tokenIDs), nil
}

// buildCloneURLs constructs both authenticated and base HTTPS clone URLs.
// Returns (authenticatedURL, baseURL, error).
// The authenticated URL contains the token and should only be shown once.
// The base URL is safe to store and display.
func (s *CheckoutService) buildCloneURLs(token, owner, repo string) (authURL, baseURL string, err error) {
	// Parse base URL
	u, err := url.Parse(s.giteaURL)
	if err != nil {
		return "", "", fmt.Errorf("parse gitea url: %w", err)
	}

	// Build base URL (no token): https://git.threesix.ai/owner/repo.git
	u.Path = fmt.Sprintf("/%s/%s.git", owner, repo)
	u.RawQuery = ""
	u.Fragment = ""
	baseURL = u.String()

	// Build authenticated URL: https://<token>@git.threesix.ai/owner/repo.git
	u.User = url.User(token)
	authURL = u.String()

	return authURL, baseURL, nil
}

// SanitizeCloneURL removes the token from a clone URL for safe logging/display.
func SanitizeCloneURL(cloneURL string) string {
	u, err := url.Parse(cloneURL)
	if err != nil {
		return cloneURL
	}
	u.User = nil
	return u.String()
}

// ExtractRepoFromURL extracts owner/repo from a git URL.
func ExtractRepoFromURL(gitURL string) (owner, repo string, err error) {
	u, err := url.Parse(gitURL)
	if err != nil {
		return "", "", err
	}

	// Remove .git suffix and leading slash
	path := strings.TrimSuffix(strings.TrimPrefix(u.Path, "/"), ".git")
	parts := strings.SplitN(path, "/", 2)
	if len(parts) != 2 {
		return "", "", fmt.Errorf("invalid git url path: %s", path)
	}

	return parts[0], parts[1], nil
}
