package httpserver

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/toki/oto/services/core/internal/cicdstate"
	"github.com/toki/oto/services/core/internal/runnerregistry"
	otopb "github.com/toki/oto/services/core/oto"
)

func handleRunnerClaimJob(store *cicdstate.Store, registry *runnerregistry.Registry, runnerID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req otopb.JobClaimRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				ErrorMessage: "invalid claim request",
			})
			return
		}

		if req.RunnerId == "" {
			req.RunnerId = runnerID
		} else if req.RunnerId != runnerID {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				ErrorMessage: "runner id mismatch between path and body",
			})
			return
		}
		if err := ensureRunnerKnown(registry, runnerID); err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				ErrorMessage: err.Error(),
			})
			return
		}

		jobID := strings.TrimSpace(req.GetJobId())
		execID := strings.TrimSpace(req.GetExecutionId())
		if jobID == "" && execID == "" {
			var foundJob *cicdstate.Job
			for _, j := range store.SnapshotJobs() {
				if j.State == cicdstate.StateQueued {
					if foundJob == nil || j.CreatedAt.Before(foundJob.CreatedAt) {
						copyJ := j
						foundJob = &copyJ
					}
				}
			}
			if foundJob == nil {
				writeResponse(w, http.StatusOK, &otopb.JobClaimResponse{
					Accepted: false,
					RunnerId: runnerID,
				})
				return
			}
			jobID = foundJob.ID
			execID = store.NextExecutionID()
		} else if jobID == "" || execID == "" {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				ErrorMessage: "job id and execution id are required",
			})
			return
		}

		job, err := store.GetJob(jobID)
		if err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ErrorMessage: "job not found",
			})
			return
		}
		if job.State != cicdstate.StateQueued {
			writeResponse(w, http.StatusConflict, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ErrorMessage: "job is not queued",
				State:        job.State,
			})
			return
		}
		if job.RunInput == nil {
			writeResponse(w, http.StatusConflict, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ErrorMessage: "job has no run request",
				State:        job.State,
			})
			return
		}
		if err := validateRunInput(job.RunInput); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ErrorMessage: err.Error(),
			})
			return
		}
		if _, err := store.CreateExecution(jobID, execID); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ExecutionId:  execID,
				ErrorMessage: err.Error(),
			})
			return
		}
		_ = store.SetExecutionRunnerID(execID, runnerID)
		if err := store.TransitionJob(jobID, cicdstate.StateRunning); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ExecutionId:  execID,
				ErrorMessage: err.Error(),
			})
			return
		}
		if err := store.TransitionExecution(execID, cicdstate.StateRunning); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.JobClaimResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ExecutionId:  execID,
				ErrorMessage: err.Error(),
			})
			return
		}

		writeResponse(w, http.StatusOK, &otopb.JobClaimResponse{
			Accepted:    true,
			RunnerId:    runnerID,
			JobId:       jobID,
			ExecutionId: execID,
			State:       cicdstate.StateRunning,
			RunRequest:  runRequestToProto(job.RunInput, runnerID, jobID, execID),
		})
	}
}

func handleRunnerReportExecution(store *cicdstate.Store, registry *runnerregistry.Registry, runnerID string, execID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req otopb.ExecutionReportRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				ErrorMessage: "invalid execution report request",
			})
			return
		}

		if req.RunnerId == "" {
			req.RunnerId = runnerID
		} else if req.RunnerId != runnerID {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				ErrorMessage: "runner id mismatch between path and body",
			})
			return
		}
		if req.ExecutionId == "" {
			req.ExecutionId = execID
		} else if req.ExecutionId != execID {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				ErrorMessage: "execution id mismatch between path and body",
			})
			return
		}
		if err := ensureRunnerKnown(registry, runnerID); err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.ExecutionReportResponse{
				RunnerId:     runnerID,
				ExecutionId:  execID,
				ErrorMessage: err.Error(),
			})
			return
		}

		exec, err := store.GetExecution(execID)
		if err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.ExecutionReportResponse{
				RunnerId:     runnerID,
				ExecutionId:  execID,
				ErrorMessage: "execution not found",
			})
			return
		}
		if exec.RunnerID != runnerID {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				RunnerId:     runnerID,
				ExecutionId:  execID,
				ErrorMessage: "execution owner mismatch",
			})
			return
		}
		jobID := strings.TrimSpace(req.GetJobId())
		if jobID == "" {
			jobID = exec.JobID
		} else if jobID != exec.JobID {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ExecutionId:  execID,
				ErrorMessage: "job id mismatch for execution",
			})
			return
		}

		targetState := cicdstate.StateFailed
		if req.GetSuccess() {
			targetState = cicdstate.StateSucceeded
		}
		if err := store.TransitionExecution(execID, targetState); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ExecutionId:  execID,
				ErrorMessage: err.Error(),
			})
			return
		}
		if err := store.TransitionJob(jobID, targetState); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.ExecutionReportResponse{
				RunnerId:     runnerID,
				JobId:        jobID,
				ExecutionId:  execID,
				ErrorMessage: err.Error(),
			})
			return
		}
		if strings.TrimSpace(req.GetMessage()) != "" {
			_ = store.AppendLog(execID, req.GetMessage())
		}

		writeResponse(w, http.StatusOK, &otopb.ExecutionReportResponse{
			Accepted:    true,
			RunnerId:    runnerID,
			JobId:       jobID,
			ExecutionId: execID,
			State:       targetState,
		})
	}
}

func handleRunnerAppendLog(store *cicdstate.Store, registry *runnerregistry.Registry, runnerID string, execID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req otopb.LogAppendRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.LogAppendResponse{
				ErrorMessage: "invalid log append request",
			})
			return
		}
		if err := normalizeRunnerExecutionRequest(registry, runnerID, execID, req.GetRunnerId(), req.GetExecutionId()); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.LogAppendResponse{
				ErrorMessage: err.Error(),
			})
			return
		}
		if err := ensureExecutionOwner(store, runnerID, execID); err != nil {
			writeResponse(w, runnerExecutionErrorStatus(err), &otopb.LogAppendResponse{
				ErrorMessage: err.Error(),
			})
			return
		}
		line := strings.TrimSpace(req.GetLine())
		if line == "" {
			writeResponse(w, http.StatusBadRequest, &otopb.LogAppendResponse{
				ErrorMessage: "line is required",
			})
			return
		}
		if err := store.AppendLog(execID, line); err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.LogAppendResponse{
				ErrorMessage: "execution not found",
			})
			return
		}
		writeResponse(w, http.StatusCreated, &otopb.LogAppendResponse{
			Accepted: true,
		})
	}
}

func handleRunnerAppendArtifact(store *cicdstate.Store, registry *runnerregistry.Registry, runnerID string, execID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req otopb.ArtifactReportRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.ArtifactReportResponse{
				ErrorMessage: "invalid artifact report request",
			})
			return
		}
		if err := normalizeRunnerExecutionRequest(registry, runnerID, execID, req.GetRunnerId(), req.GetExecutionId()); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.ArtifactReportResponse{
				ErrorMessage: err.Error(),
			})
			return
		}
		if err := ensureExecutionOwner(store, runnerID, execID); err != nil {
			writeResponse(w, runnerExecutionErrorStatus(err), &otopb.ArtifactReportResponse{
				ErrorMessage: err.Error(),
			})
			return
		}
		name := strings.TrimSpace(req.GetName())
		path := strings.TrimSpace(req.GetPath())
		if name == "" || path == "" {
			writeResponse(w, http.StatusBadRequest, &otopb.ArtifactReportResponse{
				ErrorMessage: "name and path are required",
			})
			return
		}
		if err := store.AppendArtifact(execID, name, path); err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.ArtifactReportResponse{
				ErrorMessage: "execution not found",
			})
			return
		}
		writeResponse(w, http.StatusCreated, &otopb.ArtifactReportResponse{
			Accepted: true,
		})
	}
}

func normalizeRunnerExecutionRequest(registry *runnerregistry.Registry, runnerID string, execID string, bodyRunnerID string, bodyExecID string) error {
	if bodyRunnerID != "" && bodyRunnerID != runnerID {
		return fmt.Errorf("runner id mismatch between path and body")
	}
	if bodyExecID != "" && bodyExecID != execID {
		return fmt.Errorf("execution id mismatch between path and body")
	}
	return ensureRunnerKnown(registry, runnerID)
}

func ensureRunnerKnown(registry *runnerregistry.Registry, runnerID string) error {
	if strings.TrimSpace(runnerID) == "" {
		return fmt.Errorf("missing runner id")
	}
	if registry == nil {
		return nil
	}
	if _, ok := registry.Snapshot(runnerID); !ok {
		return fmt.Errorf("runner not found")
	}
	return nil
}

func ensureExecutionOwner(store *cicdstate.Store, runnerID string, execID string) error {
	exec, err := store.GetExecution(execID)
	if err != nil {
		return fmt.Errorf("execution not found")
	}
	if exec.RunnerID != runnerID {
		return fmt.Errorf("execution owner mismatch")
	}
	return nil
}

func runnerExecutionErrorStatus(err error) int {
	if err != nil && err.Error() == "execution not found" {
		return http.StatusNotFound
	}
	return http.StatusBadRequest
}

func handleRunnerCancelExecution(store *cicdstate.Store, registry *runnerregistry.Registry, dispatcher runnerControlDispatcher, runnerID string, execID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req otopb.CancelRunRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.CancelRunResponse{
				Success:      false,
				ErrorMessage: "invalid cancel request",
			})
			return
		}

		if err := normalizeRunnerExecutionRequest(registry, runnerID, execID, req.GetRunnerId(), req.GetExecutionId()); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.CancelRunResponse{
				Success:      false,
				ErrorMessage: err.Error(),
			})
			return
		}

		exec, err := store.GetExecution(execID)
		if err != nil {
			writeResponse(w, http.StatusNotFound, &otopb.CancelRunResponse{
				Success:      false,
				ErrorMessage: "execution not found",
			})
			return
		}

		if exec.RunnerID == "" || exec.RunnerID != runnerID {
			writeResponse(w, http.StatusBadRequest, &otopb.CancelRunResponse{
				Success:      false,
				ErrorMessage: "execution owner mismatch",
			})
			return
		}

		if err := store.CancelJobExecution(exec.JobID, execID); err != nil {
			writeResponse(w, http.StatusBadRequest, &otopb.CancelRunResponse{
				Success:      false,
				ErrorMessage: err.Error(),
			})
			return
		}

		if dispatcher != nil {
			_ = dispatcher.CancelExecution(runnerID, execID, req.GetReason())
		}

		writeResponse(w, http.StatusOK, &otopb.CancelRunResponse{
			Success: true,
		})
	}
}

func handleRunnerStatus(store *cicdstate.Store, registry *runnerregistry.Registry, runnerID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		record, ok := registry.Snapshot(runnerID)
		if !ok {
			writeResponse(w, http.StatusNotFound, runnerStatusRejectedToJSON("runner not found"))
			return
		}

		var lastExec *cicdstate.Execution
		for _, exec := range store.SnapshotExecutions() {
			if exec.RunnerID == runnerID {
				if lastExec == nil || exec.CreatedAt.After(lastExec.CreatedAt) {
					copyExec := exec
					lastExec = &copyExec
				}
			}
		}

		writeResponse(w, http.StatusOK, runnerStatusToJSON(record, runnerID, lastExec))
	}
}

func handleRunnerSelfUpdate(store *cicdstate.Store, registry *runnerregistry.Registry, runnerID string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req otopb.SelfUpdateRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeResponse(w, http.StatusBadRequest, selfUpdateToJSON(false, false, false, "", "invalid self-update request"))
			return
		}

		if req.RunnerId == "" {
			req.RunnerId = runnerID
		} else if req.RunnerId != runnerID {
			writeResponse(w, http.StatusBadRequest, selfUpdateToJSON(false, false, false, "", "runner id mismatch between path and body"))
			return
		}

		if err := ensureRunnerKnown(registry, runnerID); err != nil {
			writeResponse(w, http.StatusNotFound, selfUpdateToJSON(false, false, false, "", err.Error()))
			return
		}

		if !strings.HasPrefix(req.GetDownloadUrl(), "https://") {
			writeResponse(w, http.StatusBadRequest, selfUpdateToJSON(false, false, false, "", "download_url must use HTTPS scheme"))
			return
		}

		hasActiveExecution := false
		for _, exec := range store.SnapshotExecutions() {
			if exec.RunnerID == runnerID && exec.State == cicdstate.StateRunning {
				hasActiveExecution = true
				break
			}
		}

		if hasActiveExecution {
			writeResponse(w, http.StatusOK, selfUpdateToJSON(false, false, true, req.GetVersion(), "deferred: runner is currently running a job; self-update remains an HTTP compatibility action"))
			return
		}

		writeResponse(w, http.StatusOK, selfUpdateToJSON(true, true, false, req.GetVersion(), "self-update compatibility request accepted; no socket self-update command was dispatched"))
	}
}
