package localai

import (
	"archive/tar"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/labstack/echo/v4"
	"github.com/mudler/LocalAI/core/config"
	"github.com/mudler/LocalAI/core/gallery"
	"github.com/mudler/LocalAI/core/schema"
	"github.com/mudler/LocalAI/core/services/finetune"
)

// StartFineTuneJobEndpoint starts a new fine-tuning job.
func StartFineTuneJobEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)

		var req schema.FineTuneJobRequest
		if err := c.Bind(&req); err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "Invalid request: " + err.Error(),
			})
		}

		if req.Model == "" {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "model is required",
			})
		}
		if req.DatasetSource == "" {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "dataset_source is required",
			})
		}

		resp, err := ftService.StartJob(c.Request().Context(), userID, req)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusCreated, resp)
	}
}

// ListFineTuneJobsEndpoint lists fine-tuning jobs for the current user.
func ListFineTuneJobsEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobs := ftService.ListJobs(userID)
		if jobs == nil {
			jobs = []*schema.FineTuneJob{}
		}
		return c.JSON(http.StatusOK, jobs)
	}
}

// GetFineTuneJobEndpoint gets a specific fine-tuning job.
func GetFineTuneJobEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		job, err := ftService.GetJob(userID, jobID)
		if err != nil {
			return c.JSON(http.StatusNotFound, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusOK, job)
	}
}

// StopFineTuneJobEndpoint stops a running fine-tuning job.
func StopFineTuneJobEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		// Check for save_checkpoint query param
		saveCheckpoint := c.QueryParam("save_checkpoint") == "true"

		err := ftService.StopJob(c.Request().Context(), userID, jobID, saveCheckpoint)
		if err != nil {
			return c.JSON(http.StatusNotFound, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusOK, map[string]string{
			"status":  "stopped",
			"message": "Fine-tuning job stopped",
		})
	}
}

// DeleteFineTuneJobEndpoint deletes a fine-tuning job and its data.
func DeleteFineTuneJobEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		err := ftService.DeleteJob(userID, jobID)
		if err != nil {
			status := http.StatusInternalServerError
			if strings.Contains(err.Error(), "not found") {
				status = http.StatusNotFound
			} else if strings.Contains(err.Error(), "cannot delete") {
				status = http.StatusConflict
			}
			return c.JSON(status, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusOK, map[string]string{
			"status":  "deleted",
			"message": "Fine-tuning job deleted",
		})
	}
}

// FineTuneProgressEndpoint streams progress updates via SSE.
func FineTuneProgressEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		// Set SSE headers
		c.Response().Header().Set("Content-Type", "text/event-stream")
		c.Response().Header().Set("Cache-Control", "no-cache")
		c.Response().Header().Set("Connection", "keep-alive")
		c.Response().WriteHeader(http.StatusOK)

		err := ftService.StreamProgress(c.Request().Context(), userID, jobID, func(event *schema.FineTuneProgressEvent) {
			data, err := json.Marshal(event)
			if err != nil {
				return
			}
			fmt.Fprintf(c.Response(), "data: %s\n\n", data)
			c.Response().Flush()
		})
		if err != nil {
			// If headers already sent, we can't send a JSON error
			fmt.Fprintf(c.Response(), "data: {\"status\":\"error\",\"message\":%q}\n\n", err.Error())
			c.Response().Flush()
		}

		return nil
	}
}

// ListCheckpointsEndpoint lists checkpoints for a job.
func ListCheckpointsEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		checkpoints, err := ftService.ListCheckpoints(c.Request().Context(), userID, jobID)
		if err != nil {
			return c.JSON(http.StatusNotFound, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusOK, map[string]any{
			"checkpoints": checkpoints,
		})
	}
}

// ExportModelEndpoint exports a model from a checkpoint.
func ExportModelEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		var req schema.ExportRequest
		if err := c.Bind(&req); err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "Invalid request: " + err.Error(),
			})
		}

		modelName, err := ftService.ExportModel(c.Request().Context(), userID, jobID, req)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusAccepted, map[string]string{
			"status":     "exporting",
			"message":    "Export started for model '" + modelName + "'",
			"model_name": modelName,
		})
	}
}

// DownloadExportedModelEndpoint streams the exported model directory as a tar.gz archive.
func DownloadExportedModelEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		userID := getUserID(c)
		jobID := c.Param("id")

		modelDir, modelName, err := ftService.GetExportedModelPath(userID, jobID)
		if err != nil {
			return c.JSON(http.StatusNotFound, map[string]string{
				"error": err.Error(),
			})
		}

		c.Response().Header().Set("Content-Type", "application/gzip")
		c.Response().Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s.tar.gz"`, modelName))
		c.Response().WriteHeader(http.StatusOK)

		gw := gzip.NewWriter(c.Response())
		defer gw.Close()

		tw := tar.NewWriter(gw)
		defer tw.Close()

		err = filepath.Walk(modelDir, func(path string, info os.FileInfo, walkErr error) error {
			if walkErr != nil {
				return walkErr
			}

			relPath, err := filepath.Rel(modelDir, path)
			if err != nil {
				return err
			}

			header, err := tar.FileInfoHeader(info, "")
			if err != nil {
				return err
			}
			header.Name = filepath.Join(modelName, relPath)

			if err := tw.WriteHeader(header); err != nil {
				return err
			}

			if info.IsDir() {
				return nil
			}

			f, err := os.Open(path)
			if err != nil {
				return err
			}
			defer f.Close()

			_, err = io.Copy(tw, f)
			return err
		})

		if err != nil {
			// Headers already sent, can't return JSON error
			return err
		}

		return nil
	}
}

// ListFineTuneBackendsEndpoint returns installed backends tagged with "fine-tuning".
func ListFineTuneBackendsEndpoint(appConfig *config.ApplicationConfig) echo.HandlerFunc {
	return func(c echo.Context) error {
		backends, err := gallery.AvailableBackends(appConfig.BackendGalleries, appConfig.SystemState)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": "failed to list backends: " + err.Error(),
			})
		}

		type backendInfo struct {
			Name        string   `json:"name"`
			Description string   `json:"description,omitempty"`
			Tags        []string `json:"tags,omitempty"`
		}

		var result []backendInfo
		for _, b := range backends {
			if !b.Installed {
				continue
			}
			hasTag := false
			for _, t := range b.Tags {
				if strings.EqualFold(t, "fine-tuning") {
					hasTag = true
					break
				}
			}
			if !hasTag {
				continue
			}
			name := b.Name
			if b.Alias != "" {
				name = b.Alias
			}
			result = append(result, backendInfo{
				Name:        name,
				Description: b.Description,
				Tags:        b.Tags,
			})
		}

		if result == nil {
			result = []backendInfo{}
		}

		return c.JSON(http.StatusOK, result)
	}
}

// UploadDatasetEndpoint handles dataset file upload.
func UploadDatasetEndpoint(ftService *finetune.FineTuneService) echo.HandlerFunc {
	return func(c echo.Context) error {
		file, err := c.FormFile("file")
		if err != nil {
			return c.JSON(http.StatusBadRequest, map[string]string{
				"error": "file is required",
			})
		}

		src, err := file.Open()
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": "failed to open file",
			})
		}
		defer src.Close()

		data, err := io.ReadAll(src)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": "failed to read file",
			})
		}

		path, err := ftService.UploadDataset(file.Filename, data)
		if err != nil {
			return c.JSON(http.StatusInternalServerError, map[string]string{
				"error": err.Error(),
			})
		}

		return c.JSON(http.StatusOK, map[string]string{
			"path": path,
		})
	}
}
