// Package service provides business logic services.
package service

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"time"

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

// SessionService orchestrates interactive remote development sessions.
// A session composes checkout (git token) + pod binding + ephemeral preview URL.
type SessionService struct {
	sessionRepo   port.SessionRepository
	checkoutSvc   *CheckoutService
	projectRepo   port.ProjectRepository
	previewMgr    port.PreviewManager
	previewDomain string
	defaultExpiry time.Duration
}

// SessionServiceConfig holds configuration for the session service.
type SessionServiceConfig struct {
	// PreviewDomain is the base domain for preview URLs (e.g., "preview.threesix.ai").
	PreviewDomain string

	// DefaultExpiry is the default session duration (default: 24h).
	DefaultExpiry time.Duration
}

// NewSessionService creates a new session service.
func NewSessionService(
	sessionRepo port.SessionRepository,
	checkoutSvc *CheckoutService,
	projectRepo port.ProjectRepository,
	previewMgr port.PreviewManager,
	cfg SessionServiceConfig,
) *SessionService {
	expiry := cfg.DefaultExpiry
	if expiry == 0 {
		expiry = 24 * time.Hour
	}
	previewDomain := cfg.PreviewDomain
	if previewDomain == "" {
		previewDomain = "preview.threesix.ai"
	}
	return &SessionService{
		sessionRepo:   sessionRepo,
		checkoutSvc:   checkoutSvc,
		projectRepo:   projectRepo,
		previewMgr:    previewMgr,
		previewDomain: previewDomain,
		defaultExpiry: expiry,
	}
}

// CreateSessionRequest contains parameters for creating a session.
type CreateSessionRequest struct {
	ProjectID   domain.ProjectID
	Branch      string
	NewBranch   string
	FromRef     string
	FeatureSlug string
	ExpiresIn   time.Duration
	PreviewPort int
	CreatedBy   string
}

// SessionResult contains the result of a session creation.
type SessionResult struct {
	Session               *domain.Session
	AuthenticatedCloneURL string
	Branch                string
	Instructions          string
}

// CreateSession creates a new interactive development session.
func (s *SessionService) CreateSession(ctx context.Context, req CreateSessionRequest) (*SessionResult, error) {
	log := logging.FromContext(ctx).WithService("SessionService")

	// Verify project exists and get pod info.
	project, err := s.projectRepo.Get(ctx, req.ProjectID)
	if err != nil {
		return nil, fmt.Errorf("get project: %w", err)
	}
	if project.PodName == "" {
		return nil, domain.ErrProjectNotRunning
	}

	// Check no active session exists for this project.
	existing, err := s.sessionRepo.GetActiveByProject(ctx, req.ProjectID)
	if err == nil && existing != nil {
		if existing.IsExpired() {
			// Cleanup the expired session inline.
			_ = s.teardownSession(ctx, existing)
		} else {
			return nil, domain.ErrSessionExists
		}
	}

	// Calculate expiry.
	expiry := s.defaultExpiry
	if req.ExpiresIn > 0 {
		expiry = req.ExpiresIn
	}

	// Create checkout (git token + branch).
	checkoutResult, err := s.checkoutSvc.Checkout(ctx, CheckoutRequest{
		ProjectID:    req.ProjectID,
		Branch:       req.Branch,
		NewBranch:    req.NewBranch,
		FromRef:      req.FromRef,
		FeatureSlug:  req.FeatureSlug,
		ExpiresIn:    expiry,
		CheckedOutBy: req.CreatedBy,
	})
	if err != nil {
		return nil, fmt.Errorf("create checkout: %w", err)
	}

	// Generate preview slug (short random hex).
	slug, err := generatePreviewSlug()
	if err != nil {
		return nil, fmt.Errorf("generate preview slug: %w", err)
	}
	previewHost := slug + "." + s.previewDomain
	previewURL := "https://" + previewHost

	// Determine preview port.
	previewPort := req.PreviewPort
	if previewPort == 0 {
		previewPort = 8080
	}

	// Create session record first to get the ID.
	session := &domain.Session{
		ProjectID:   req.ProjectID,
		CheckoutID:  checkoutResult.Checkout.ID,
		PodName:     project.PodName,
		PreviewURL:  previewURL,
		PreviewHost: previewHost,
		CreatedBy:   req.CreatedBy,
		CreatedAt:   time.Now(),
		ExpiresAt:   time.Now().Add(expiry),
		Status:      domain.SessionStatusActive,
	}

	if err := s.sessionRepo.Create(ctx, session); err != nil {
		// Rollback: revoke checkout token.
		_ = s.checkoutSvc.Revoke(ctx, checkoutResult.Checkout.ID)
		return nil, fmt.Errorf("create session record: %w", err)
	}

	// Create K8s preview (Service + Ingress).
	if err := s.previewMgr.CreatePreview(ctx, port.PreviewOptions{
		SessionID: string(session.ID),
		Namespace: "rdev",
		PodName:   project.PodName,
		Host:      previewHost,
		Port:      previewPort,
	}); err != nil {
		// Rollback: mark session ended and revoke checkout.
		_ = s.sessionRepo.SetEnded(ctx, session.ID)
		_ = s.checkoutSvc.Revoke(ctx, checkoutResult.Checkout.ID)
		return nil, fmt.Errorf("create preview: %w", err)
	}

	log.Info("session created",
		logging.FieldProjectID, req.ProjectID,
		"session_id", session.ID,
		"preview_url", previewURL,
		"pod", project.PodName,
		"branch", checkoutResult.Checkout.Branch,
	)

	branch := checkoutResult.Checkout.Branch
	instructions := fmt.Sprintf(`Session started.

Preview URL: %s
Clone URL: %s
Branch: %s
Pod: %s

Run commands via:
  POST /projects/%s/claude  (Claude Code commands)
  POST /projects/%s/shell   (shell commands)
  GET  /projects/%s/stream  (SSE output)

End session:
  POST /projects/%s/sessions/%s/checkin

Session expires: %s`,
		previewURL,
		checkoutResult.AuthenticatedCloneURL,
		branch,
		project.PodName,
		req.ProjectID, req.ProjectID, req.ProjectID,
		req.ProjectID, session.ID,
		session.ExpiresAt.Format(time.RFC3339),
	)

	return &SessionResult{
		Session:               session,
		AuthenticatedCloneURL: checkoutResult.AuthenticatedCloneURL,
		Branch:                branch,
		Instructions:          instructions,
	}, nil
}

// EndSessionRequest contains parameters for ending a session.
type EndSessionRequest struct {
	SessionID  domain.SessionID
	SkipReview bool
	AutoMerge  bool
}

// EndSessionResult contains the result of ending a session.
type EndSessionResult struct {
	SessionID    domain.SessionID
	ReviewTaskID string
	Status       domain.SessionStatus
}

// EndSession ends an active session, tearing down preview and completing checkout.
func (s *SessionService) EndSession(ctx context.Context, req EndSessionRequest) (*EndSessionResult, error) {
	log := logging.FromContext(ctx).WithService("SessionService")

	session, err := s.sessionRepo.Get(ctx, req.SessionID)
	if err != nil {
		return nil, err
	}
	if session.Status != domain.SessionStatusActive {
		return nil, domain.ErrSessionNotActive
	}

	// Delete preview (Service + Ingress).
	if err := s.previewMgr.DeletePreview(ctx, string(session.ID)); err != nil {
		log.Warn("failed to delete preview",
			"session_id", session.ID,
			logging.FieldError, err,
		)
		// Continue — preview cleanup failure shouldn't block checkin.
	}

	// Complete checkout (revoke token, queue review).
	var reviewTaskID string
	checkinResult, err := s.checkoutSvc.Checkin(ctx, CheckinRequest{
		CheckoutID: session.CheckoutID,
		SkipReview: req.SkipReview,
		AutoMerge:  req.AutoMerge,
	})
	if err != nil {
		log.Warn("failed to checkin checkout",
			"session_id", session.ID,
			"checkout_id", session.CheckoutID,
			logging.FieldError, err,
		)
		// Continue — we still want to mark the session ended.
	} else {
		reviewTaskID = checkinResult.ReviewTaskID
	}

	// Mark session ended.
	if err := s.sessionRepo.SetEnded(ctx, session.ID); err != nil {
		return nil, fmt.Errorf("set session ended: %w", err)
	}

	log.Info("session ended",
		logging.FieldProjectID, session.ProjectID,
		"session_id", session.ID,
		"review_queued", reviewTaskID != "",
	)

	return &EndSessionResult{
		SessionID:    session.ID,
		ReviewTaskID: reviewTaskID,
		Status:       domain.SessionStatusEnded,
	}, nil
}

// Get retrieves a session by ID.
func (s *SessionService) Get(ctx context.Context, id domain.SessionID) (*domain.Session, error) {
	return s.sessionRepo.Get(ctx, id)
}

// ListByProject returns all sessions for a project.
func (s *SessionService) ListByProject(ctx context.Context, projectID domain.ProjectID) ([]*domain.Session, error) {
	return s.sessionRepo.ListByProject(ctx, projectID)
}

// ForceEnd forcefully ends a session without checkout checkin (admin use).
func (s *SessionService) ForceEnd(ctx context.Context, id domain.SessionID) error {
	log := logging.FromContext(ctx).WithService("SessionService")

	session, err := s.sessionRepo.Get(ctx, id)
	if err != nil {
		return err
	}
	if session.Status != domain.SessionStatusActive {
		return domain.ErrSessionNotActive
	}

	// Delete preview.
	if err := s.previewMgr.DeletePreview(ctx, string(session.ID)); err != nil {
		log.Warn("failed to delete preview on force-end",
			"session_id", session.ID,
			logging.FieldError, err,
		)
	}

	// Revoke checkout token.
	if err := s.checkoutSvc.Revoke(ctx, session.CheckoutID); err != nil {
		log.Warn("failed to revoke checkout on force-end",
			"session_id", session.ID,
			"checkout_id", session.CheckoutID,
			logging.FieldError, err,
		)
	}

	// Mark session ended.
	if err := s.sessionRepo.SetEnded(ctx, session.ID); err != nil {
		return fmt.Errorf("set session ended: %w", err)
	}

	log.Info("session force-ended",
		logging.FieldProjectID, session.ProjectID,
		"session_id", session.ID,
	)

	return nil
}

// CleanupExpired finds and tears down expired sessions.
// Returns the number of sessions cleaned up.
func (s *SessionService) CleanupExpired(ctx context.Context) (int, error) {
	log := logging.FromContext(ctx).WithService("SessionService")

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

	for _, session := range sessions {
		// Delete preview resources.
		if err := s.previewMgr.DeletePreview(ctx, string(session.ID)); err != nil {
			log.Warn("failed to delete preview for expired session",
				"session_id", session.ID,
				logging.FieldError, err,
			)
		}

		// Revoke checkout token (the checkout cleanup may also do this).
		_ = s.checkoutSvc.Revoke(ctx, session.CheckoutID)
	}

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

	return len(sessions), nil
}

// teardownSession handles cleanup of an expired session found inline.
func (s *SessionService) teardownSession(ctx context.Context, session *domain.Session) error {
	if err := s.previewMgr.DeletePreview(ctx, string(session.ID)); err != nil {
		return err
	}
	return s.sessionRepo.SetEnded(ctx, session.ID)
}

// generatePreviewSlug generates a short random hex string for preview URLs.
func generatePreviewSlug() (string, error) {
	b := make([]byte, 6) // 12 hex chars
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("generate random bytes: %w", err)
	}
	return hex.EncodeToString(b), nil
}
