package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/gofiber/fiber/v2/middleware/recover"
)

var safePathTokenRe = regexp.MustCompile(`[^a-zA-Z0-9._-]+`)

func safePathToken(s string) string {
	s = strings.TrimSpace(s)
	s = strings.ReplaceAll(s, string(filepath.Separator), "_")
	// In case the client sent URL-like separators.
	s = strings.ReplaceAll(s, "/", "_")
	s = strings.ReplaceAll(s, "\\", "_")
	s = safePathTokenRe.ReplaceAllString(s, "_")
	s = strings.Trim(s, "._-")
	if s == "" || s == "." {
		return ""
	}
	if len(s) > 128 {
		s = s[:128]
	}
	return s
}

func safeFilename(name string) string {
	name = strings.TrimSpace(name)
	name = filepath.Base(name)
	if name == "" || name == "." || name == ".." {
		return ""
	}
	// Disallow any remaining separators just in case.
	name = strings.ReplaceAll(name, string(filepath.Separator), "_")
	name = strings.ReplaceAll(name, "/", "_")
	name = strings.ReplaceAll(name, "\\", "_")
	if name == "" || name == "." || name == ".." {
		return ""
	}
	if len(name) > 255 {
		name = name[:255]
	}
	return name
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8082"
	}

	app := fiber.New(fiber.Config{
		DisableStartupMessage: true,
	})

	app.Use(recover.New())
	app.Use(logger.New())
	app.Use(cors.New(cors.Config{
		AllowOrigins: "*",
		AllowHeaders: "Origin, Content-Type, Accept, Authorization",
		AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS",
	}))

	// Ensure preflight requests are handled.
	app.Options("/*", func(c *fiber.Ctx) error {
		return c.SendStatus(fiber.StatusNoContent)
	})

	app.Get("/health", func(c *fiber.Ctx) error {
		return c.Status(fiber.StatusOK).SendString("ok")
	})

	// Serve built SPA from disk (no embedding).
	// Expected layout:
	//   server/static/index.html
	//   server/static/assets/...
	app.Static("/", "./static")

	app.Post("/upload", func(c *fiber.Ctx) error {
		form, err := c.MultipartForm()
		if err != nil {
			return fiber.NewError(fiber.StatusBadRequest, "invalid multipart form")
		}

		user := safePathToken(c.FormValue("user"))
		session := safePathToken(c.FormValue("session"))
		prop := safePathToken(c.FormValue("prop"))
		if user == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing form field: user")
		}
		if session == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing form field: session")
		}

		files := form.File["documents"]
		if len(files) == 0 {
			return fiber.NewError(fiber.StatusBadRequest, "missing files: documents")
		}

		dir := filepath.Join("docs", user, session)
		if prop != "" {
			dir = filepath.Join(dir, prop)
		}
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fiber.NewError(fiber.StatusInternalServerError, "cannot create upload directory")
		}

		uploaded := make([]string, 0, len(files))

		for _, file := range files {
			name := safeFilename(file.Filename)
			if name == "" {
				return fiber.NewError(fiber.StatusBadRequest, "invalid filename")
			}

			dst := filepath.Join(dir, name)
			if err := c.SaveFile(file, dst); err != nil {
				return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("cannot save file %s", name))
			}
			uploaded = append(uploaded, name)
		}

		return c.JSON(fiber.Map{
			"ok":    true,
			"files": uploaded,
		})
	})

	app.Post("/loadattachments", func(c *fiber.Ctx) error {
		// Accept either JSON body { id, session } or form fields.
		var body struct {
			ID      string `json:"id"`
			Session string `json:"session"`
			Prop    string `json:"prop"`
		}
		_ = c.BodyParser(&body)

		id := safePathToken(body.ID)
		if id == "" {
			id = safePathToken(c.FormValue("id"))
		}
		session := safePathToken(body.Session)
		if session == "" {
			session = safePathToken(c.FormValue("session"))
		}
		prop := safePathToken(body.Prop)
		if prop == "" {
			prop = safePathToken(c.FormValue("prop"))
		}

		if id == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: id")
		}
		if session == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: session")
		}

		dir := filepath.Join("docs", id, session)
		if prop != "" {
			dir = filepath.Join(dir, prop)
		}
		entries, err := os.ReadDir(dir)
		if err != nil {
			if os.IsNotExist(err) {
				return c.JSON([]string{})
			}
			return fiber.NewError(fiber.StatusInternalServerError, "cannot read attachments directory")
		}

		files := make([]string, 0, len(entries))
		for _, e := range entries {
			if e.IsDir() {
				continue
			}
			name := e.Name()
			if name == "" || name == "." {
				continue
			}
			files = append(files, name)
		}

		return c.JSON(files)
	})

	app.Post("/loadattachmentslist", func(c *fiber.Ctx) error {
		// Returns a JSON object keyed by the actual subfolder name.
		// Example: { "salaryCertificate": ["file1.pdf"], "avsCertificate": ["file2.pdf"] }
		// It scans docs/<id>/<session>/ and for each subdirectory lists its files.
		var body struct {
			ID      string `json:"id"`
			Session string `json:"session"`
		}
		_ = c.BodyParser(&body)

		id := safePathToken(body.ID)
		if id == "" {
			id = safePathToken(c.FormValue("id"))
		}
		session := safePathToken(body.Session)
		if session == "" {
			session = safePathToken(c.FormValue("session"))
		}

		if id == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: id")
		}
		if session == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: session")
		}

		baseDir := filepath.Join("docs", id, session)
		entries, err := os.ReadDir(baseDir)
		if err != nil {
			if os.IsNotExist(err) {
				return c.JSON(fiber.Map{})
			}
			return fiber.NewError(fiber.StatusInternalServerError, "cannot read session directory")
		}

		result := make(map[string][]string)
		for _, entry := range entries {
			if !entry.IsDir() {
				continue
			}
			folderName := entry.Name()
			if safePathToken(folderName) == "" {
				continue
			}

			propDir := filepath.Join(baseDir, folderName)
			propEntries, err := os.ReadDir(propDir)
			if err != nil {
				// Ignore unreadable prop folder.
				continue
			}

			files := make([]string, 0, len(propEntries))
			for _, pe := range propEntries {
				if pe.IsDir() {
					continue
				}
				name := pe.Name()
				if name == "" || name == "." {
					continue
				}
				files = append(files, name)
			}
			result[folderName] = files
		}

		return c.JSON(result)
	})

	app.Post("/deleteattachment", func(c *fiber.Ctx) error {
		// Accept either JSON body { id, session, filename } or form fields.
		var body struct {
			ID       string `json:"id"`
			Session  string `json:"session"`
			Prop     string `json:"prop"`
			Filename string `json:"filename"`
		}
		_ = c.BodyParser(&body)

		id := safePathToken(body.ID)
		if id == "" {
			id = safePathToken(c.FormValue("id"))
		}
		session := safePathToken(body.Session)
		if session == "" {
			session = safePathToken(c.FormValue("session"))
		}
		prop := safePathToken(body.Prop)
		if prop == "" {
			prop = safePathToken(c.FormValue("prop"))
		}
		filename := safeFilename(body.Filename)
		if filename == "" {
			filename = safeFilename(c.FormValue("filename"))
		}

		if id == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: id")
		}
		if session == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: session")
		}
		if filename == "" {
			return fiber.NewError(fiber.StatusBadRequest, "missing field: filename")
		}

		path := filepath.Join("docs", id, session)
		if prop != "" {
			path = filepath.Join(path, prop)
		}
		path = filepath.Join(path, filename)
		err := os.Remove(path)
		if err != nil {
			if os.IsNotExist(err) {
				return c.JSON(fiber.Map{
					"ok":      true,
					"deleted": false,
					"file":    filename,
				})
			}
			return fiber.NewError(fiber.StatusInternalServerError, "cannot delete attachment")
		}

		return c.JSON(fiber.Map{
			"ok":      true,
			"deleted": true,
			"file":    filename,
		})
	})

	// History API fallback for SPA routes.
	app.Use(func(c *fiber.Ctx) error {
		return c.SendFile("./static/index.html")
	})

	addr := ":" + port
	log.Printf("Starting Fiber server on %s", addr)
	if err := app.Listen(addr); err != nil {
		log.Fatalf("server failed: %v", err)
	}
}
