package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"git.casaderoll.de/michael/urbm/internal/model"
	"git.casaderoll.de/michael/urbm/internal/service"
)

type Server struct {
	service *service.Service
	log     *slog.Logger
	http    *http.Server
	socket  string
}

var Version = "dev"

func New(socket string, svc *service.Service, log *slog.Logger) *Server {
	s := &Server{service: svc, log: log, socket: socket}
	mux := http.NewServeMux()
	mux.HandleFunc("GET /v1/health", s.health)
	mux.HandleFunc("GET /v1/config", s.getConfig)
	mux.HandleFunc("PUT /v1/config", s.putConfig)
	mux.HandleFunc("PUT /v1/secrets/{id}", s.putSecret)
	mux.HandleFunc("DELETE /v1/secrets/{id}", s.deleteSecret)
	mux.HandleFunc("GET /v1/runs", s.runs)
	mux.HandleFunc("DELETE /v1/runs", s.clearRuns)
	mux.HandleFunc("GET /v1/filesystem/directories", s.directories)
	mux.HandleFunc("GET /v1/workloads/{kind}", s.workloads)
	mux.HandleFunc("POST /v1/jobs/{id}/run", s.runJob)
	mux.HandleFunc("POST /v1/runs/{id}/cancel", s.cancelRun)
	mux.HandleFunc("POST /v1/runs/{id}/pause", s.pauseRun)
	mux.HandleFunc("POST /v1/runs/{id}/resume", s.resumeRun)
	mux.HandleFunc("POST /v1/repositories/{id}/test", s.testRepository)
	mux.HandleFunc("POST /v1/repositories/{id}/init", s.initRepository)
	mux.HandleFunc("POST /v1/repositories/{id}/unlock", s.unlockRepository)
	mux.HandleFunc("POST /v1/repositories/{id}/{action}", s.maintenance)
	mux.HandleFunc("GET /v1/repositories/stats", s.repositoryStats)
	mux.HandleFunc("GET /v1/repositories/{id}/snapshots", s.snapshots)
	mux.HandleFunc("GET /v1/repositories/{id}/snapshots/{snapshot}/files", s.snapshotFiles)
	mux.HandleFunc("POST /v1/restores", s.restore)
	s.http = &http.Server{Handler: requestLog(log, mux), ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 5 * time.Minute, IdleTimeout: 30 * time.Second}
	return s
}

func (s *Server) ListenAndServe() error {
	if err := os.MkdirAll(filepath.Dir(s.socket), 0750); err != nil {
		return err
	}
	_ = os.Remove(s.socket)
	listener, err := net.Listen("unix", s.socket)
	if err != nil {
		return err
	}
	if err := os.Chmod(s.socket, 0660); err != nil {
		listener.Close()
		return err
	}
	err = s.http.Serve(listener)
	if errors.Is(err, http.ErrServerClosed) {
		return nil
	}
	return err
}

func (s *Server) Shutdown(ctx context.Context) error {
	defer os.Remove(s.socket)
	return s.http.Shutdown(ctx)
}

func (s *Server) health(w http.ResponseWriter, _ *http.Request) {
	writeJSON(w, 200, map[string]any{"status": "ok", "version": Version, "schemaVersion": model.SchemaVersion})
}
func (s *Server) getConfig(w http.ResponseWriter, _ *http.Request) {
	writeJSON(w, 200, s.service.Config())
}

func (s *Server) putConfig(w http.ResponseWriter, r *http.Request) {
	var config model.Config
	if err := decode(r, &config); err != nil {
		writeError(w, err)
		return
	}
	if err := s.service.SaveConfig(config); err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, config)
}

func (s *Server) putSecret(w http.ResponseWriter, r *http.Request) {
	var body struct {
		Type  string `json:"type"`
		Value string `json:"value"`
	}
	if err := decode(r, &body); err != nil {
		writeError(w, err)
		return
	}
	if err := s.service.PutSecret(r.PathValue("id"), body.Type, body.Value); err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 204, nil)
}
func (s *Server) deleteSecret(w http.ResponseWriter, r *http.Request) {
	if err := s.service.DeleteSecret(r.PathValue("id")); err != nil {
		writeError(w, err)
		return
	}
	w.WriteHeader(204)
}
func (s *Server) runs(w http.ResponseWriter, _ *http.Request) { writeJSON(w, 200, s.service.Runs()) }
func (s *Server) clearRuns(w http.ResponseWriter, _ *http.Request) {
	writeJSON(w, 200, map[string]int{"cleared": s.service.ClearRunHistory()})
}
func (s *Server) directories(w http.ResponseWriter, r *http.Request) {
	items, err := s.service.BrowseDirectories(r.URL.Query().Get("path"))
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, items)
}
func (s *Server) workloads(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
	defer cancel()
	items, err := s.service.DiscoverWorkloads(ctx, model.JobType(r.PathValue("kind")))
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, items)
}

func (s *Server) runJob(w http.ResponseWriter, r *http.Request) {
	run, err := s.service.EnqueueJob(r.PathValue("id"), 10)
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 202, run)
}
func (s *Server) cancelRun(w http.ResponseWriter, r *http.Request) {
	if !s.service.Cancel(r.PathValue("id")) {
		writeJSON(w, 404, map[string]string{"error": "run not found"})
		return
	}
	w.WriteHeader(204)
}

func (s *Server) pauseRun(w http.ResponseWriter, r *http.Request) {
	if err := s.service.Pause(r.PathValue("id")); err != nil {
		writeError(w, err)
		return
	}
	w.WriteHeader(204)
}

func (s *Server) resumeRun(w http.ResponseWriter, r *http.Request) {
	if err := s.service.Resume(r.PathValue("id")); err != nil {
		writeError(w, err)
		return
	}
	w.WriteHeader(204)
}

func (s *Server) testRepository(w http.ResponseWriter, r *http.Request) {
	s.repositoryAction(w, r, false)
}
func (s *Server) initRepository(w http.ResponseWriter, r *http.Request) {
	s.repositoryAction(w, r, true)
}
func (s *Server) unlockRepository(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
	defer cancel()
	if err := s.service.UnlockRepository(ctx, r.PathValue("id")); err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, map[string]bool{"ok": true})
}
func (s *Server) repositoryAction(w http.ResponseWriter, r *http.Request, initialize bool) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
	defer cancel()
	if err := s.service.TestRepository(ctx, r.PathValue("id"), initialize); err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, map[string]bool{"ok": true})
}

func (s *Server) maintenance(w http.ResponseWriter, r *http.Request) {
	run, err := s.service.EnqueueMaintenance(r.PathValue("id"), r.PathValue("action"))
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 202, run)
}

func (s *Server) snapshots(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
	defer cancel()
	items, err := s.service.Snapshots(ctx, r.PathValue("id"))
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, items)
}

func (s *Server) repositoryStats(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute)
	defer cancel()
	writeJSON(w, 200, s.service.RepositoryStats(ctx))
}
func (s *Server) snapshotFiles(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
	defer cancel()
	items, err := s.service.SnapshotFiles(ctx, r.PathValue("id"), r.PathValue("snapshot"), r.URL.Query().Get("path"))
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 200, items)
}
func (s *Server) restore(w http.ResponseWriter, r *http.Request) {
	var task model.RestoreTask
	if err := decode(r, &task); err != nil {
		writeError(w, err)
		return
	}
	run, err := s.service.EnqueueRestore(task)
	if err != nil {
		writeError(w, err)
		return
	}
	writeJSON(w, 202, run)
}

func decode(r *http.Request, target any) error {
	decoder := json.NewDecoder(io.LimitReader(r.Body, 2*1024*1024))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return fmt.Errorf("validation: invalid JSON: %w", err)
	}
	return nil
}

func writeJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if status != 204 && value != nil {
		_ = json.NewEncoder(w).Encode(value)
	}
}
func writeError(w http.ResponseWriter, err error) {
	status := 500
	message := err.Error()
	if strings.HasPrefix(message, "validation:") {
		status = 400
	}
	if strings.Contains(message, "unknown") {
		status = 404
	}
	if strings.Contains(message, "already queued") {
		status = 409
	}
	writeJSON(w, status, map[string]string{"error": message})
}

func requestLog(log *slog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		started := time.Now()
		next.ServeHTTP(w, r)
		log.Info("api request", "method", r.Method, "path", r.URL.Path, "durationMs", time.Since(started).Milliseconds())
	})
}
