package handlers

import (
	"context"
	"errors"
	"net/http"
	"strconv"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/orchard9/rdev/internal/auth"
	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/service"
	"github.com/orchard9/rdev/internal/validate"
	"github.com/orchard9/rdev/pkg/api"
)

// SessionsHandler handles interactive remote development session endpoints.
type SessionsHandler struct {
	sessionService *service.SessionService
}

// NewSessionsHandler creates a new sessions handler.
func NewSessionsHandler(sessionService *service.SessionService) *SessionsHandler {
	return &SessionsHandler{sessionService: sessionService}
}

// Mount registers the session routes.
func (h *SessionsHandler) Mount(r api.Router) {
	r.Route("/projects/{id}/sessions", func(r chi.Router) {
		// List sessions (read access)
		r.With(auth.RequireScope(auth.ScopeSessionsRead, auth.ScopeProjectsRead, auth.ScopeAdmin)).
			Get("/", h.List)

		// Create session (execute access)
		r.With(auth.RequireScope(auth.ScopeSessionsExecute, auth.ScopeProjectsExecute, auth.ScopeAdmin)).
			Post("/", h.Create)

		// Get session (read access)
		r.With(auth.RequireScope(auth.ScopeSessionsRead, auth.ScopeProjectsRead, auth.ScopeAdmin)).
			Get("/{sid}", h.Get)

		// End session via checkin (execute access)
		r.With(auth.RequireScope(auth.ScopeSessionsExecute, auth.ScopeProjectsExecute, auth.ScopeAdmin)).
			Post("/{sid}/checkin", h.Checkin)

		// Force-terminate session (admin only)
		r.With(auth.RequireScope(auth.ScopeAdmin)).
			Delete("/{sid}", h.Delete)
	})
}

// CreateSessionRequest is the JSON body for creating a session.
type CreateSessionRequest struct {
	Branch      string `json:"branch,omitempty"`
	NewBranch   string `json:"new_branch,omitempty"`
	FromRef     string `json:"from_ref,omitempty"`
	FeatureSlug string `json:"feature_slug,omitempty"`
	ExpiresIn   string `json:"expires_in,omitempty"`   // Duration string (e.g., "24h", "7d")
	PreviewPort int    `json:"preview_port,omitempty"` // Default: 8080
}

// SessionResponse is the JSON response for a session.
type SessionResponse struct {
	ID           string  `json:"id"`
	ProjectID    string  `json:"project_id"`
	CheckoutID   string  `json:"checkout_id"`
	PodName      string  `json:"pod_name"`
	PreviewURL   string  `json:"preview_url"`
	Status       string  `json:"status"`
	CreatedBy    string  `json:"created_by"`
	CreatedAt    string  `json:"created_at"`
	ExpiresAt    string  `json:"expires_at"`
	EndedAt      *string `json:"ended_at,omitempty"`
	AuthCloneURL string  `json:"auth_clone_url,omitempty"` // Only at creation
	Branch       string  `json:"branch,omitempty"`         // Only at creation
	Instructions string  `json:"instructions,omitempty"`   // Only at creation
}

// SessionCheckinRequest is the JSON body for ending a session.
type SessionCheckinRequest struct {
	SkipReview bool `json:"skip_review,omitempty"`
	AutoMerge  bool `json:"auto_merge,omitempty"`
}

// SessionCheckinResponse is the JSON response for ending a session.
type SessionCheckinResponse struct {
	SessionID    string `json:"session_id"`
	Status       string `json:"status"`
	ReviewTaskID string `json:"review_task_id,omitempty"`
	Message      string `json:"message"`
}

// List returns all sessions for a project.
// GET /projects/{id}/sessions
func (h *SessionsHandler) List(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if err := domain.ValidateProjectID(projectID); err != nil {
		api.WriteBadRequest(w, r, "invalid project id")
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), TimeoutFastLookup)
	defer cancel()

	sessions, err := h.sessionService.ListByProject(ctx, domain.ProjectID(projectID))
	if err != nil {
		api.WriteInternalError(w, r, "Failed to list sessions")
		return
	}

	resp := make([]SessionResponse, len(sessions))
	for i, s := range sessions {
		resp[i] = sessionToResponse(s)
	}

	api.WriteSuccess(w, r, map[string]any{
		"sessions": resp,
	})
}

// Create creates a new interactive development session.
// POST /projects/{id}/sessions
func (h *SessionsHandler) Create(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if err := domain.ValidateProjectID(projectID); err != nil {
		api.WriteBadRequest(w, r, "invalid project id")
		return
	}

	var req CreateSessionRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		api.WriteBadRequest(w, r, "invalid request body")
		return
	}

	// Validate: need either branch or new_branch.
	if req.Branch == "" && req.NewBranch == "" {
		api.WriteBadRequest(w, r, "branch or new_branch is required")
		return
	}
	if req.Branch != "" && req.NewBranch != "" {
		api.WriteBadRequest(w, r, "specify either branch or new_branch, not both")
		return
	}

	// Validate branch name if provided.
	if req.NewBranch != "" {
		if err := validate.Name(req.NewBranch, "new_branch"); err != nil {
			api.WriteBadRequest(w, r, err.Error())
			return
		}
	}

	// Parse expiry duration.
	var expiresIn time.Duration
	if req.ExpiresIn != "" {
		var err error
		expiresIn, err = time.ParseDuration(req.ExpiresIn)
		if err != nil {
			// Try parsing as days (e.g., "7d").
			if len(req.ExpiresIn) > 1 && req.ExpiresIn[len(req.ExpiresIn)-1] == 'd' {
				days, parseErr := strconv.Atoi(req.ExpiresIn[:len(req.ExpiresIn)-1])
				if parseErr == nil && days > 0 && days <= 30 {
					expiresIn = time.Duration(days) * 24 * time.Hour
				} else {
					api.WriteBadRequest(w, r, "expires_in must be a valid duration (e.g., 24h, 7d)")
					return
				}
			} else {
				api.WriteBadRequest(w, r, "expires_in must be a valid duration (e.g., 24h, 7d)")
				return
			}
		}
		if expiresIn > 30*24*time.Hour {
			api.WriteBadRequest(w, r, "expires_in cannot exceed 30 days")
			return
		}
	}

	// Get user from API key.
	createdBy := "unknown"
	if apiKey := auth.GetAPIKey(r.Context()); apiKey != nil {
		createdBy = string(apiKey.ID)
	}

	ctx, cancel := context.WithTimeout(r.Context(), TimeoutOrchestration)
	defer cancel()

	result, err := h.sessionService.CreateSession(ctx, service.CreateSessionRequest{
		ProjectID:   domain.ProjectID(projectID),
		Branch:      req.Branch,
		NewBranch:   req.NewBranch,
		FromRef:     req.FromRef,
		FeatureSlug: req.FeatureSlug,
		ExpiresIn:   expiresIn,
		PreviewPort: req.PreviewPort,
		CreatedBy:   createdBy,
	})
	if err != nil {
		if errors.Is(err, domain.ErrProjectNotFound) {
			api.WriteNotFound(w, r, "project not found")
			return
		}
		if errors.Is(err, domain.ErrProjectNotRunning) {
			api.WriteBadRequest(w, r, "project pod is not running")
			return
		}
		if errors.Is(err, domain.ErrSessionExists) {
			api.WriteError(w, r, http.StatusConflict, "SESSION_EXISTS",
				"active session already exists for this project")
			return
		}
		if errors.Is(err, domain.ErrBranchNotFound) {
			api.WriteNotFound(w, r, "branch not found")
			return
		}
		if errors.Is(err, domain.ErrBranchProtected) {
			api.WriteBadRequest(w, r, "cannot checkout protected branch")
			return
		}
		if errors.Is(err, domain.ErrCheckoutAlreadyExists) {
			api.WriteError(w, r, http.StatusConflict, "CHECKOUT_EXISTS",
				"active checkout already exists for this branch")
			return
		}
		api.WriteInternalError(w, r, "Failed to create session")
		return
	}

	resp := sessionToResponse(result.Session)
	resp.AuthCloneURL = result.AuthenticatedCloneURL
	resp.Branch = result.Branch
	resp.Instructions = result.Instructions
	api.WriteCreated(w, r, resp)
}

// Get retrieves a session by ID.
// GET /projects/{id}/sessions/{sid}
func (h *SessionsHandler) Get(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if err := domain.ValidateProjectID(projectID); err != nil {
		api.WriteBadRequest(w, r, "invalid project id")
		return
	}

	sid := chi.URLParam(r, "sid")
	if sid == "" {
		api.WriteBadRequest(w, r, "session id is required")
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), TimeoutFastLookup)
	defer cancel()

	session, err := h.sessionService.Get(ctx, domain.SessionID(sid))
	if err != nil {
		if errors.Is(err, domain.ErrSessionNotFound) {
			api.WriteNotFound(w, r, "session not found")
			return
		}
		api.WriteInternalError(w, r, "Failed to get session")
		return
	}

	// Verify session belongs to project.
	if string(session.ProjectID) != projectID {
		api.WriteNotFound(w, r, "session not found")
		return
	}

	api.WriteSuccess(w, r, sessionToResponse(session))
}

// Checkin ends a session and optionally queues a review.
// POST /projects/{id}/sessions/{sid}/checkin
func (h *SessionsHandler) Checkin(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if err := domain.ValidateProjectID(projectID); err != nil {
		api.WriteBadRequest(w, r, "invalid project id")
		return
	}

	sid := chi.URLParam(r, "sid")
	if sid == "" {
		api.WriteBadRequest(w, r, "session id is required")
		return
	}

	var req SessionCheckinRequest
	if err := api.DecodeJSON(r, &req); err != nil {
		req = SessionCheckinRequest{}
	}

	ctx, cancel := context.WithTimeout(r.Context(), TimeoutStandard)
	defer cancel()

	// Verify session exists and belongs to project.
	session, err := h.sessionService.Get(ctx, domain.SessionID(sid))
	if err != nil {
		if errors.Is(err, domain.ErrSessionNotFound) {
			api.WriteNotFound(w, r, "session not found")
			return
		}
		api.WriteInternalError(w, r, "Failed to get session")
		return
	}
	if string(session.ProjectID) != projectID {
		api.WriteNotFound(w, r, "session not found")
		return
	}

	result, err := h.sessionService.EndSession(ctx, service.EndSessionRequest{
		SessionID:  domain.SessionID(sid),
		SkipReview: req.SkipReview,
		AutoMerge:  req.AutoMerge,
	})
	if err != nil {
		if errors.Is(err, domain.ErrSessionNotFound) {
			api.WriteNotFound(w, r, "session not found")
			return
		}
		if errors.Is(err, domain.ErrSessionNotActive) {
			api.WriteBadRequest(w, r, "session is not active")
			return
		}
		api.WriteInternalError(w, r, "Failed to end session")
		return
	}

	message := "Session ended. Preview removed, token revoked."
	if result.ReviewTaskID != "" {
		message = "Session ended. Review task queued."
	}

	api.WriteSuccess(w, r, SessionCheckinResponse{
		SessionID:    string(result.SessionID),
		Status:       string(result.Status),
		ReviewTaskID: result.ReviewTaskID,
		Message:      message,
	})
}

// Delete force-terminates a session (admin only).
// DELETE /projects/{id}/sessions/{sid}
func (h *SessionsHandler) Delete(w http.ResponseWriter, r *http.Request) {
	projectID := chi.URLParam(r, "id")
	if err := domain.ValidateProjectID(projectID); err != nil {
		api.WriteBadRequest(w, r, "invalid project id")
		return
	}

	sid := chi.URLParam(r, "sid")
	if sid == "" {
		api.WriteBadRequest(w, r, "session id is required")
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), TimeoutStandard)
	defer cancel()

	// Verify session exists and belongs to project.
	session, err := h.sessionService.Get(ctx, domain.SessionID(sid))
	if err != nil {
		if errors.Is(err, domain.ErrSessionNotFound) {
			api.WriteNotFound(w, r, "session not found")
			return
		}
		api.WriteInternalError(w, r, "Failed to get session")
		return
	}
	if string(session.ProjectID) != projectID {
		api.WriteNotFound(w, r, "session not found")
		return
	}

	if err := h.sessionService.ForceEnd(ctx, domain.SessionID(sid)); err != nil {
		if errors.Is(err, domain.ErrSessionNotFound) {
			api.WriteNotFound(w, r, "session not found")
			return
		}
		if errors.Is(err, domain.ErrSessionNotActive) {
			api.WriteBadRequest(w, r, "session is not active")
			return
		}
		api.WriteInternalError(w, r, "Failed to terminate session")
		return
	}

	api.WriteSuccess(w, r, map[string]string{
		"status":  "terminated",
		"id":      sid,
		"message": "Session force-terminated. Preview removed, token revoked.",
	})
}

// sessionToResponse converts a domain session to a response.
func sessionToResponse(s *domain.Session) SessionResponse {
	resp := SessionResponse{
		ID:         string(s.ID),
		ProjectID:  string(s.ProjectID),
		CheckoutID: string(s.CheckoutID),
		PodName:    s.PodName,
		PreviewURL: s.PreviewURL,
		Status:     string(s.Status),
		CreatedBy:  s.CreatedBy,
		CreatedAt:  s.CreatedAt.Format(time.RFC3339),
		ExpiresAt:  s.ExpiresAt.Format(time.RFC3339),
	}
	if s.EndedAt != nil {
		t := s.EndedAt.Format(time.RFC3339)
		resp.EndedAt = &t
	}
	return resp
}
