package main

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"golang.org/x/crypto/ssh"
)

// ─── 持久化数据类型 ───────────────────────────────────────────────

type SSHProfile struct {
	Name       string `json:"name,omitempty"`
	Alias      string `json:"alias"`
	Host       string `json:"host"`
	Port       int    `json:"port"`
	Username   string `json:"username"`
	Password   string `json:"password,omitempty"`
	PrivateKey string `json:"privateKey,omitempty"`
	Passphrase string `json:"passphrase,omitempty"`
}

type Command struct {
	Alias   string `json:"alias"`
	Command string `json:"command"`
}

type ScriptInfo struct {
	Name    string `json:"name"`
	Content string `json:"content,omitempty"`
}

// 配置与数据库辅助函数见 config.go / db.go

type wsMessage struct {
	Type       string `json:"type"`
	Host       string `json:"host,omitempty"`
	Port       int    `json:"port,omitempty"`
	Username   string `json:"username,omitempty"`
	Password   string `json:"password,omitempty"`
	PrivateKey string `json:"privateKey,omitempty"`
	Passphrase string `json:"passphrase,omitempty"`
	Data       string `json:"data,omitempty"`
	Cols       int    `json:"cols,omitempty"`
	Rows       int    `json:"rows,omitempty"`
	Status     string `json:"status,omitempty"`
	Message    string `json:"message,omitempty"`
}

type wsWriter struct {
	conn *websocket.Conn
	mu   sync.Mutex
}

func (w *wsWriter) send(msg wsMessage) {
	w.mu.Lock()
	defer w.mu.Unlock()
	_ = w.conn.WriteJSON(msg)
}

// ─── 会话令牌（服务启动时随机生成一次）────────────────────────────────

var sessionToken string

func initSessionToken() {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		log.Fatalf("failed to generate session token: %v", err)
	}
	sessionToken = hex.EncodeToString(b)
}

// authMiddleware 校验请求中携带的令牌。
// WebSocket 不支持自定义 Header，因此同时接受 ?token= 查询参数。
func authMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		token := ""
		if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
			token = strings.TrimPrefix(auth, "Bearer ")
		}
		if token == "" {
			token = c.Query("token")
		}
		if token != sessionToken {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
			return
		}
		c.Next()
	}
}

// POST /api/auth/login — 用密码换取令牌
func handleLogin(c *gin.Context) {
	var body struct {
		Password string `json:"password"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	if body.Password != accessPassword() {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "密码错误"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": gin.H{"token": sessionToken}})
}

// GET /api/auth/verify — 校验令牌是否有效（受 authMiddleware 保护）
func handleVerify(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"data": gin.H{"valid": true}})
}

// GET / — 服务说明（无需鉴权，便于网关 / 探活）
func handleAPIRoot(c *gin.Context) {
	loc := time.FixedZone("CST", 8*3600)
	c.JSON(http.StatusOK, gin.H{
		"description": "提供 Web SSH 终端与跳板连接（SSH 由服务端发起）；浏览器访问需先 POST /api/auth/login 换取令牌，后续 API 与 WebSocket 携带 Bearer 或 ?token=",
		"endpoints": gin.H{
			"health":       "/health",
			"auth":         "/api/auth/login (POST，公开), /api/auth/verify (GET，需令牌)",
			"websocket_ssh": "/api/ws/ssh (GET 升级 WebSocket，需令牌；查询参数 token= 或 Header)",
			"ssh_profiles": "/api/ssh, /api/ssh/:name (CRUD，需令牌)",
			"commands":     "/api/commands, /api/commands/:index (CRUD，需令牌)",
			"scripts":      "/api/scripts, /api/scripts/:name (CRUD，需令牌)",
		},
		"message":   "萌芽 SSH MengyaConnect 后端 API 服务运行中",
		"timestamp": time.Now().In(loc).Format(time.RFC3339),
		"version":   apiVersion(),
	})
}

func main() {
	initSessionToken()
	if err := initDB(); err != nil {
		log.Fatalf("db init: %v", err)
	}
	migrateFromFiles()

	if mode := os.Getenv("GIN_MODE"); mode != "" {
		gin.SetMode(mode)
	}

	router := gin.New()
	router.Use(gin.Logger(), gin.Recovery(), corsMiddleware())

	allowedOrigins := parseListEnv("ALLOWED_ORIGINS")
	upgrader := websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			return isOriginAllowed(r.Header.Get("Origin"), allowedOrigins)
		},
	}

	// ─── 不需要鉴权的路由 ────────────────────────────────────────
	router.GET("/", handleAPIRoot)
	router.GET("/health", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"status": "ok",
			"time":   time.Now().Format(time.RFC3339),
		})
	})
	router.POST("/api/auth/login", handleLogin)

	// ─── 需要鉴权的路由 ──────────────────────────────────────────
	auth := router.Group("/api", authMiddleware())

	auth.GET("/auth/verify", handleVerify)

	auth.GET("/ws/ssh", func(c *gin.Context) {
		handleSSHWebSocket(c, upgrader)
	})

	// SSH 配置 CRUD
	auth.GET("/ssh", handleListSSH)
	auth.POST("/ssh", handleCreateSSH)
	auth.PUT("/ssh/:name", handleUpdateSSH)
	auth.DELETE("/ssh/:name", handleDeleteSSH)

	// 快捷命令 CRUD
	auth.GET("/commands", handleListCommands)
	auth.POST("/commands", handleCreateCommand)
	auth.PUT("/commands/:index", handleUpdateCommand)
	auth.DELETE("/commands/:index", handleDeleteCommand)

	// 脚本 CRUD
	auth.GET("/scripts", handleListScripts)
	auth.GET("/scripts/:name", handleGetScript)
	auth.POST("/scripts", handleCreateScript)
	auth.PUT("/scripts/:name", handleUpdateScript)
	auth.DELETE("/scripts/:name", handleDeleteScript)

	addr := getEnv("ADDR", ":"+getEnv("PORT", "8080"))
	server := &http.Server{
		Addr:              addr,
		Handler:           router,
		ReadHeaderTimeout: 10 * time.Second,
	}

	log.Printf("SSH WebSocket server listening on %s", addr)
	log.Printf("Access password configured (use ACCESS_PASSWORD env to override)")
	if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatalf("server error: %v", err)
	}
}

func handleSSHWebSocket(c *gin.Context, upgrader websocket.Upgrader) {
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		return
	}
	defer conn.Close()
	conn.SetReadLimit(1 << 20)

	writer := &wsWriter{conn: conn}
	writer.send(wsMessage{Type: "status", Status: "connected", Message: "WebSocket connected"})

	var (
		sshClient  *ssh.Client
		sshSession *ssh.Session
		sshStdin   io.WriteCloser
		stdout     io.Reader
		stderr     io.Reader
		cancelFn   context.CancelFunc
	)

	cleanup := func() {
		if cancelFn != nil {
			cancelFn()
		}
		if sshSession != nil {
			_ = sshSession.Close()
		}
		if sshClient != nil {
			_ = sshClient.Close()
		}
	}
	defer cleanup()

	for {
		var msg wsMessage
		if err := conn.ReadJSON(&msg); err != nil {
			writer.send(wsMessage{Type: "status", Status: "closed", Message: "WebSocket closed"})
			return
		}

		switch msg.Type {
		case "connect":
			if sshSession != nil {
				writer.send(wsMessage{Type: "error", Message: "SSH session already exists"})
				continue
			}

			client, session, stdin, out, errOut, err := startSSHSession(msg)
			if err != nil {
				writer.send(wsMessage{Type: "error", Message: err.Error()})
				continue
			}

			sshClient = client
			sshSession = session
			sshStdin = stdin
			stdout = out
			stderr = errOut

			ctx, cancel := context.WithCancel(context.Background())
			cancelFn = cancel
			go streamToWebSocket(ctx, writer, stdout)
			go streamToWebSocket(ctx, writer, stderr)
			go func() {
				_ = session.Wait()
				writer.send(wsMessage{Type: "status", Status: "closed", Message: "SSH session closed"})
				cleanup()
			}()

			writer.send(wsMessage{Type: "status", Status: "ready", Message: "SSH connected"})

		case "input":
			if sshStdin == nil {
				writer.send(wsMessage{Type: "error", Message: "SSH session not ready"})
				continue
			}
			if msg.Data != "" {
				_, _ = sshStdin.Write([]byte(msg.Data))
			}

		case "resize":
			if sshSession == nil {
				continue
			}
			rows := msg.Rows
			cols := msg.Cols
			if rows > 0 && cols > 0 {
				_ = sshSession.WindowChange(rows, cols)
			}

		case "ping":
			writer.send(wsMessage{Type: "pong"})

		case "close":
			writer.send(wsMessage{Type: "status", Status: "closing", Message: "Closing SSH session"})
			return
		}
	}
}

func startSSHSession(msg wsMessage) (*ssh.Client, *ssh.Session, io.WriteCloser, io.Reader, io.Reader, error) {
	host := strings.TrimSpace(msg.Host)
	if host == "" {
		return nil, nil, nil, nil, nil, errors.New("host is required")
	}
	port := msg.Port
	if port == 0 {
		port = 22
	}
	user := strings.TrimSpace(msg.Username)
	if user == "" {
		return nil, nil, nil, nil, nil, errors.New("username is required")
	}

	auths, err := buildAuthMethods(msg)
	if err != nil {
		return nil, nil, nil, nil, nil, err
	}

	cfg := &ssh.ClientConfig{
		User:            user,
		Auth:            auths,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         12 * time.Second,
	}

	addr := fmt.Sprintf("%s:%d", host, port)
	client, err := ssh.Dial("tcp", addr, cfg)
	if err != nil {
		return nil, nil, nil, nil, nil, fmt.Errorf("ssh dial failed: %w", err)
	}

	session, err := client.NewSession()
	if err != nil {
		_ = client.Close()
		return nil, nil, nil, nil, nil, fmt.Errorf("ssh session failed: %w", err)
	}

	rows := msg.Rows
	cols := msg.Cols
	if rows == 0 {
		rows = 24
	}
	if cols == 0 {
		cols = 80
	}

	modes := ssh.TerminalModes{
		ssh.ECHO:          1,
		ssh.TTY_OP_ISPEED: 14400,
		ssh.TTY_OP_OSPEED: 14400,
	}
	if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil {
		_ = session.Close()
		_ = client.Close()
		return nil, nil, nil, nil, nil, fmt.Errorf("request pty failed: %w", err)
	}

	stdin, err := session.StdinPipe()
	if err != nil {
		_ = session.Close()
		_ = client.Close()
		return nil, nil, nil, nil, nil, fmt.Errorf("stdin pipe failed: %w", err)
	}

	stdout, err := session.StdoutPipe()
	if err != nil {
		_ = session.Close()
		_ = client.Close()
		return nil, nil, nil, nil, nil, fmt.Errorf("stdout pipe failed: %w", err)
	}

	stderr, err := session.StderrPipe()
	if err != nil {
		_ = session.Close()
		_ = client.Close()
		return nil, nil, nil, nil, nil, fmt.Errorf("stderr pipe failed: %w", err)
	}

	if err := session.Shell(); err != nil {
		_ = session.Close()
		_ = client.Close()
		return nil, nil, nil, nil, nil, fmt.Errorf("shell start failed: %w", err)
	}

	return client, session, stdin, stdout, stderr, nil
}

func buildAuthMethods(msg wsMessage) ([]ssh.AuthMethod, error) {
	var methods []ssh.AuthMethod
	if strings.TrimSpace(msg.PrivateKey) != "" {
		signer, err := parsePrivateKey(msg.PrivateKey, msg.Passphrase)
		if err != nil {
			return nil, fmt.Errorf("private key error: %w", err)
		}
		methods = append(methods, ssh.PublicKeys(signer))
	}
	if msg.Password != "" {
		methods = append(methods, ssh.Password(msg.Password))
	}
	if len(methods) == 0 {
		return nil, errors.New("no auth method provided")
	}
	return methods, nil
}

func parsePrivateKey(key, passphrase string) (ssh.Signer, error) {
	key = strings.TrimSpace(key)
	if passphrase != "" {
		return ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(passphrase))
	}
	return ssh.ParsePrivateKey([]byte(key))
}

func streamToWebSocket(ctx context.Context, writer *wsWriter, reader io.Reader) {
	buf := make([]byte, 8192)
	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		n, err := reader.Read(buf)
		if n > 0 {
			writer.send(wsMessage{Type: "output", Data: string(buf[:n])})
		}
		if err != nil {
			return
		}
	}
}

// ═══════════════════════════════════════════════════════════════════
// SSH 配置 CRUD
// ═══════════════════════════════════════════════════════════════════

// GET /api/ssh
func handleListSSH(c *gin.Context) {
	rows, err := DB.Query(
		`SELECT name,alias,host,port,username,password,private_key,passphrase
		 FROM ssh_profiles ORDER BY id`)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
		return
	}
	defer rows.Close()
	profiles := []SSHProfile{}
	for rows.Next() {
		var p SSHProfile
		if err := rows.Scan(&p.Name, &p.Alias, &p.Host, &p.Port,
			&p.Username, &p.Password, &p.PrivateKey, &p.Passphrase); err != nil {
			continue
		}
		profiles = append(profiles, p)
	}
	c.JSON(http.StatusOK, gin.H{"data": profiles})
}

// POST /api/ssh
func handleCreateSSH(c *gin.Context) {
	var p SSHProfile
	if err := c.ShouldBindJSON(&p); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	if p.Alias == "" || p.Host == "" || p.Username == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
		return
	}
	name := strings.TrimSpace(p.Name)
	if name == "" {
		name = p.Alias
	}
	name = strings.ReplaceAll(name, " ", "-")
	if err := validateName(name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	if p.Port == 0 {
		p.Port = 22
	}
	_, err := DB.Exec(
		`INSERT INTO ssh_profiles (name,alias,host,port,username,password,private_key,passphrase)
		 VALUES (?,?,?,?,?,?,?,?)`,
		name, p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase,
	)
	if err != nil {
		if strings.Contains(err.Error(), "UNIQUE") {
			c.JSON(http.StatusConflict, gin.H{"error": "name already exists"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
		}
		return
	}
	p.Name = name
	c.JSON(http.StatusOK, gin.H{"data": p})
}

// PUT /api/ssh/:name
func handleUpdateSSH(c *gin.Context) {
	name := c.Param("name")
	if err := validateName(name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	var p SSHProfile
	if err := c.ShouldBindJSON(&p); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	if p.Alias == "" || p.Host == "" || p.Username == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
		return
	}
	if p.Port == 0 {
		p.Port = 22
	}
	res, err := DB.Exec(
		`UPDATE ssh_profiles
		 SET alias=?,host=?,port=?,username=?,password=?,private_key=?,passphrase=?,
		     updated_at=CURRENT_TIMESTAMP
		 WHERE name=?`,
		p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase, name,
	)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}
	p.Name = name
	c.JSON(http.StatusOK, gin.H{"data": p})
}

// DELETE /api/ssh/:name
func handleDeleteSSH(c *gin.Context) {
	name := c.Param("name")
	if err := validateName(name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	res, err := DB.Exec(`DELETE FROM ssh_profiles WHERE name=?`, name)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}

// ═══════════════════════════════════════════════════════════════════
// 快捷命令 CRUD
// ═══════════════════════════════════════════════════════════════════

func queryAllCommands() ([]Command, error) {
	rows, err := DB.Query(`SELECT alias,command FROM commands ORDER BY sort_order, id`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	cmds := []Command{}
	for rows.Next() {
		var cmd Command
		if err := rows.Scan(&cmd.Alias, &cmd.Command); err != nil {
			return nil, err
		}
		cmds = append(cmds, cmd)
	}
	return cmds, nil
}

// commandIDAtIndex returns the primary-key id of the command at 0-based position idx.
func commandIDAtIndex(idx int) (int64, error) {
	rows, err := DB.Query(`SELECT id FROM commands ORDER BY sort_order, id`)
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	pos := 0
	for rows.Next() {
		var id int64
		if err := rows.Scan(&id); err != nil {
			return 0, err
		}
		if pos == idx {
			return id, nil
		}
		pos++
	}
	return 0, sql.ErrNoRows
}

// GET /api/commands
func handleListCommands(c *gin.Context) {
	cmds, err := queryAllCommands()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": cmds})
}

// POST /api/commands
func handleCreateCommand(c *gin.Context) {
	var cmd Command
	if err := c.ShouldBindJSON(&cmd); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	if cmd.Alias == "" || cmd.Command == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
		return
	}
	var maxOrder int
	_ = DB.QueryRow(`SELECT COALESCE(MAX(sort_order),0) FROM commands`).Scan(&maxOrder)
	if _, err := DB.Exec(
		`INSERT INTO commands (alias,command,sort_order) VALUES (?,?,?)`,
		cmd.Alias, cmd.Command, maxOrder+1,
	); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
		return
	}
	cmds, _ := queryAllCommands()
	c.JSON(http.StatusOK, gin.H{"data": cmds})
}

// PUT /api/commands/:index
func handleUpdateCommand(c *gin.Context) {
	idx, err := strconv.Atoi(c.Param("index"))
	if err != nil || idx < 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"})
		return
	}
	var cmd Command
	if err := c.ShouldBindJSON(&cmd); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	if cmd.Alias == "" || cmd.Command == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
		return
	}
	id, err := commandIDAtIndex(idx)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "index out of range"})
		return
	}
	if _, err := DB.Exec(`UPDATE commands SET alias=?,command=? WHERE id=?`, cmd.Alias, cmd.Command, id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
		return
	}
	cmds, _ := queryAllCommands()
	c.JSON(http.StatusOK, gin.H{"data": cmds})
}

// DELETE /api/commands/:index
func handleDeleteCommand(c *gin.Context) {
	idx, err := strconv.Atoi(c.Param("index"))
	if err != nil || idx < 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"})
		return
	}
	id, err := commandIDAtIndex(idx)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "index out of range"})
		return
	}
	if _, err := DB.Exec(`DELETE FROM commands WHERE id=?`, id); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
		return
	}
	cmds, _ := queryAllCommands()
	c.JSON(http.StatusOK, gin.H{"data": cmds})
}

// ═══════════════════════════════════════════════════════════════════
// 脚本 CRUD
// ═══════════════════════════════════════════════════════════════════

// GET /api/scripts
func handleListScripts(c *gin.Context) {
	rows, err := DB.Query(`SELECT name FROM scripts ORDER BY name`)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"data": []ScriptInfo{}})
		return
	}
	defer rows.Close()
	scripts := []ScriptInfo{}
	for rows.Next() {
		var s ScriptInfo
		if err := rows.Scan(&s.Name); err != nil {
			continue
		}
		scripts = append(scripts, s)
	}
	c.JSON(http.StatusOK, gin.H{"data": scripts})
}

// GET /api/scripts/:name
func handleGetScript(c *gin.Context) {
	name := c.Param("name")
	if err := validateName(name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	var s ScriptInfo
	err := DB.QueryRow(`SELECT name,content FROM scripts WHERE name=?`, name).
		Scan(&s.Name, &s.Content)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	} else if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": s})
}

// POST /api/scripts
func handleCreateScript(c *gin.Context) {
	var s ScriptInfo
	if err := c.ShouldBindJSON(&s); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	if s.Name == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "name 为必填项"})
		return
	}
	if err := validateName(s.Name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	_, err := DB.Exec(`INSERT INTO scripts (name,content) VALUES (?,?)`, s.Name, s.Content)
	if err != nil {
		if strings.Contains(err.Error(), "UNIQUE") {
			c.JSON(http.StatusConflict, gin.H{"error": "name already exists"})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
		}
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": s})
}

// PUT /api/scripts/:name
func handleUpdateScript(c *gin.Context) {
	name := c.Param("name")
	if err := validateName(name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	var s ScriptInfo
	if err := c.ShouldBindJSON(&s); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
		return
	}
	res, err := DB.Exec(
		`UPDATE scripts SET content=?,updated_at=CURRENT_TIMESTAMP WHERE name=?`,
		s.Content, name,
	)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: s.Content}})
}

// DELETE /api/scripts/:name
func handleDeleteScript(c *gin.Context) {
	name := c.Param("name")
	if err := validateName(name); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
		return
	}
	res, err := DB.Exec(`DELETE FROM scripts WHERE name=?`, name)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
		return
	}
	if n, _ := res.RowsAffected(); n == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}
	c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
