package agent

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"mime"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"tea.chunkbyte.com/kato/go-worm/lib/command"
	"tea.chunkbyte.com/kato/go-worm/lib/config"
	"tea.chunkbyte.com/kato/go-worm/lib/files"
	"tea.chunkbyte.com/kato/go-worm/lib/helpers"
	"tea.chunkbyte.com/kato/go-worm/lib/models"
	"tea.chunkbyte.com/kato/go-worm/lib/screenshot"
)

func (a *Agent) handleHealth(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	helpers.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	helpers.WriteJSON(w, http.StatusOK, map[string]any{
		"openapi": "3.0.3", "info": map[string]string{"title": "Local Management Agent", "version": config.Version},
		"paths": map[string]any{
			"/api/v1/status":     map[string]any{"get": map[string]string{"summary": "Agent status"}},
			"/api/v1/files":      map[string]any{"get": map[string]string{"summary": "List files"}},
			"/api/v1/download":   map[string]any{"get": map[string]string{"summary": "Download file"}},
			"/api/v1/screenshot": map[string]any{"get": map[string]string{"summary": "Capture desktop"}},
			"/api/v1/exec":       map[string]any{"post": map[string]string{"summary": "Run a command"}},
		},
	})
}

func (a *Agent) handleStatus(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	host, _ := os.Hostname()
	helpers.WriteJSON(w, http.StatusOK, map[string]any{
		"os": "windows", "architecture": runtime.GOARCH, "user": helpers.Username(), "hostname": host,
		"uptime_seconds": int64(time.Since(a.startedAt).Seconds()), "local_ips": helpers.LocalIPs(),
		"agent_version": config.Version, "listen_address": a.addr,
	})
}

func (a *Agent) handleFiles(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	depth, err := files.ParseDepth(r.URL.Query().Get("depth"))
	if err != nil {
		helpers.WriteError(w, http.StatusBadRequest, err.Error())
		return
	}
	path := r.URL.Query().Get("path")
	if path == "" {
		path, _ = os.UserHomeDir()
	}
	dir, err := files.AllowedPath(a.root, path, true)
	if err != nil {
		files.WritePathError(w, err)
		return
	}
	info, err := os.Stat(dir)
	if err != nil {
		files.WritePathError(w, err)
		return
	}
	if !info.IsDir() {
		helpers.WriteError(w, http.StatusBadRequest, "path is not a directory")
		return
	}
	entries, err := files.ListDirectory(dir, depth)
	if err != nil {
		files.WritePathError(w, err)
		return
	}
	helpers.WriteJSON(w, http.StatusOK, map[string]any{"path": dir, "depth": depth, "entries": entries})
}

func (a *Agent) handleDownload(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	path := r.URL.Query().Get("path")
	if path == "" {
		helpers.WriteError(w, http.StatusBadRequest, "path is required")
		return
	}
	file, err := files.AllowedPath(a.root, path, true)
	if err != nil {
		files.WritePathError(w, err)
		return
	}
	f, err := os.Open(file)
	if err != nil {
		files.WritePathError(w, err)
		return
	}
	defer f.Close()
	info, err := f.Stat()
	if err != nil {
		files.WritePathError(w, err)
		return
	}
	if info.IsDir() {
		helpers.WriteError(w, http.StatusBadRequest, "path is a directory")
		return
	}
	name := filepath.Base(file)
	contentType := mime.TypeByExtension(filepath.Ext(name))
	if contentType == "" {
		var sample [512]byte
		n, _ := f.Read(sample[:])
		contentType = http.DetectContentType(sample[:n])
		_, _ = f.Seek(0, io.SeekStart)
	}
	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Disposition", `attachment; filename="`+strings.ReplaceAll(name, `"`, "'")+`"`)
	w.Header().Set("Accept-Ranges", "bytes")
	http.ServeContent(w, r, name, info.ModTime(), f)
}

func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	format := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("format")))
	if format == "" {
		format = "png"
	}
	if format != "png" && format != "jpeg" {
		helpers.WriteError(w, http.StatusBadRequest, "format must be png or jpeg")
		return
	}
	quality := 80
	if raw := r.URL.Query().Get("quality"); raw != "" {
		var err error
		quality, err = strconv.Atoi(raw)
		if err != nil || quality < 1 || quality > config.MaxImageQuality {
			helpers.WriteError(w, http.StatusBadRequest, "quality must be between 1 and 100")
			return
		}
	}
	images, err := screenshot.Capture(format, quality)
	if err != nil {
		helpers.Log.Printf("screenshot: %v", err)
		helpers.WriteError(w, http.StatusServiceUnavailable, "no interactive desktop is available")
		return
	}
	if len(images) == 1 {
		w.Header().Set("Content-Type", images[0].ContentType)
		w.Header().Set("Content-Length", strconv.Itoa(len(images[0].Data)))
		_, _ = w.Write(images[0].Data)
		return
	}
	response := models.ScreenshotResponse{Images: make([]models.ScreenshotImage, 0, len(images))}
	for i, item := range images {
		response.Images = append(response.Images, models.ScreenshotImage{
			Monitor:     i,
			ContentType: item.ContentType,
			DataBase64:  base64.StdEncoding.EncodeToString(item.Data),
		})
	}
	helpers.WriteJSON(w, http.StatusOK, response)
}

func (a *Agent) handleExec(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
		return
	}
	r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax)
	defer r.Body.Close()
	var request models.ExecRequest
	decoder := json.NewDecoder(r.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&request); err != nil {
		helpers.WriteError(w, http.StatusBadRequest, "body must contain a command")
		return
	}
	cmdline := strings.TrimSpace(request.Command)
	if cmdline == "" {
		helpers.WriteError(w, http.StatusBadRequest, command.ErrEmptyCommand.Error())
		return
	}
	timeout, err := command.ResolveTimeout(request.TimeoutSec)
	if err != nil {
		helpers.WriteError(w, http.StatusBadRequest, err.Error())
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), timeout)
	defer cancel()
	result, err := command.Run(ctx, cmdline)
	if errors.Is(err, command.ErrTimeout) {
		helpers.WriteError(w, http.StatusGatewayTimeout, err.Error())
		return
	}
	if err != nil {
		helpers.Log.Printf("exec: %v", err)
		helpers.WriteError(w, http.StatusInternalServerError, "could not run command")
		return
	}
	helpers.WriteJSON(w, http.StatusOK, result)
}
