package main

import (
	"canguidev/shelfy/internal/db"
	"canguidev/shelfy/internal/routes"
	"canguidev/shelfy/internal/utils"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"embed"
	"encoding/pem"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
)

// --------- CONFIG À ADAPTER ----------

var (
	// Dossier racine SFTP (tu montes déjà ton volume ici dans Docker)
	SFTPBaseDir = "upload"

	// Identifiants standards (utilisés si IP non autorisée)
	LoginUser = "cangui2089"
	LoginPass = "GHT30k7!"

	// IP(s) autorisées pour connexion SANS mot de passe
	AllowedIPs = []string{
		"82.65.73.115",
	}
)

// -------------------------------------

// ---------- SFTP Server ----------

// startSFTPServer démarre un serveur SFTP sur le port 2222
// avec authentification par IP-allowlist et user/pass.
// Le dossier racine SFTP est défini par `base`.
// Les clés hôte sont générées ou chargées depuis le disque.
// Les IP autorisées peuvent se connecter sans mot de passe.
// Les autres doivent utiliser le login et mot de passe définis.
// Le serveur écoute sur le port 2222 et gère les connexions SFTP.
// Il utilise les clés hôte ed25519 et RSA pour la compatibilité.
// Les connexions TCP ont un keep-alive agressif pour éviter les coupures NAT.
// Les erreurs de connexion sont loguées, mais le serveur continue à accepter les connexions.
// Les requêtes SFTP sont traitées avec un serveur SFTP minimaliste.
// Les erreurs de traitement des requêtes sont loguées.
// Le serveur est conçu pour être lancé dans un goroutine séparé.
// Il vérifie que le dossier racine existe et est un répertoire valide.
// Les clés hôte sont stockées dans le même répertoire que le serveur SFTP.
// Le serveur utilise le protocole SSH pour établir les connexions SFTP.
// Il gère les canaux de session et les requêtes de sous-système SFTP.
// Les connexions sont acceptées en boucle infinie, avec des logs pour chaque connexion réussie ou échouée.
// Les erreurs de handshake SSH sont loguées et la connexion est fermée.
// Les canaux SFTP sont acceptés et traités pour servir les requêtes SFTP.
// Le serveur SFTP est initialisé avec des gestionnaires par défaut pour les opérations de fichier.
// Les erreurs lors de l'initialisation ou du traitement des requêtes SFTP sont loguées.
// Le serveur continue à accepter les connexions même après des erreurs, garantissant une disponibilité maximale.
// Il est recommandé de lancer cette fonction dans un goroutine séparé pour ne pas bloquer l'exécution principale du programme.


func startSFTPServer(base string) {
	// --- helpers locaux pour charger/générer les clés hôte ---
	loadOrCreateEd25519 := func(path string) (ssh.Signer, error) {
		if _, err := os.Stat(path); err == nil {
			b, err := os.ReadFile(path)
			if err != nil { return nil, err }
			return ssh.ParsePrivateKey(b)
		}
		_, priv, err := ed25519.GenerateKey(rand.Reader)
		if err != nil { return nil, err }
		pkcs8, err := x509.MarshalPKCS8PrivateKey(priv)
		if err != nil { return nil, err }
		pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
		if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err }
		return ssh.ParsePrivateKey(pemBytes)
	}
	loadOrCreateRSA := func(path string) (ssh.Signer, error) {
		if _, err := os.Stat(path); err == nil {
			b, err := os.ReadFile(path)
			if err != nil { return nil, err }
			return ssh.ParsePrivateKey(b)
		}
		priv, err := rsa.GenerateKey(rand.Reader, 2048)
		if err != nil { return nil, err }
		pkcs1 := x509.MarshalPKCS1PrivateKey(priv)
		pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1})
		if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err }
		return ssh.ParsePrivateKey(pemBytes)
	}
	ipAllowed := func(addr string) bool {
		host, _, err := net.SplitHostPort(addr)
		if err != nil { host = addr }
		for _, allow := range AllowedIPs {
			if host == allow { return true }
		}
		return false
	}

	// --- vérifications et "ancrage" dans le dossier upload ---
	absBase, _ := filepath.Abs(base)
	if fi, err := os.Stat(absBase); err != nil || !fi.IsDir() {
		log.Fatalf("[SFTP] Le dossier %s est manquant ou invalide: %v", absBase, err)
	}
	if err := os.Chdir(absBase); err != nil {
		log.Fatalf("[SFTP] Chdir(%s): %v", absBase, err)
	}

	// --- clés hôte : ed25519 (moderne) + RSA (compat) ---
	signerED, err := loadOrCreateEd25519("sftp_host_ed25519.pem")
	if err != nil { log.Fatalf("[SFTP] Host key ed25519 error: %v", err) }
	signerRSA, err := loadOrCreateRSA("sftp_host_rsa.pem")
	if err != nil { log.Fatalf("[SFTP] Host key RSA error: %v", err) }

	// --- config SSH avec auth IP-allowlist et user/pass ---
	cfg := &ssh.ServerConfig{
		PasswordCallback: func(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
			remote := meta.RemoteAddr().String()
			user := meta.User()

			// IP autorisée → on accepte même mot de passe vide
			if ipAllowed(remote) {
				log.Printf("[SFTP] Auth IP-allowlist OK from=%s user=%s (mdp vide accepté)", remote, user)
				return nil, nil
			}
			// Sinon, exige user+pass
			if user == LoginUser && string(pass) == LoginPass {
				log.Printf("[SFTP] Auth OK from=%s user=%s", remote, user)
				return nil, nil
			}
			log.Printf("[SFTP] Auth FAIL from=%s user=%s", remote, user)
			return nil, fmt.Errorf("auth failed")
		},
	}
	cfg.AddHostKey(signerED)
	cfg.AddHostKey(signerRSA)

	// (option compat avancée) : décommente seulement si un client antique échoue encore
	// cfg.Config = ssh.Config{
	// 	KeyExchanges: []string{
	// 		"curve25519-sha256", "curve25519-sha256@libssh.org",
	// 		"diffie-hellman-group14-sha1",
	// 	},
	// 	Ciphers: []string{
	// 		"chacha20-poly1305@openssh.com",
	// 		"aes128-ctr","aes192-ctr","aes256-ctr",
	// 		"aes128-cbc",
	// 	},
	// 	MACs: []string{"hmac-sha2-256","hmac-sha2-512","hmac-sha1"},
	// }

	// --- listener TCP avec keep-alive agressif ---
	ln, err := net.Listen("tcp", ":2222")
	if err != nil { log.Fatalf("[SFTP] Listen: %v", err) }
	log.Printf("[SFTP] Écoute sur sftp://%s@0.0.0.0:2222 (root=%s)", LoginUser, absBase)
	log.Printf("[SFTP] IP sans mot de passe autorisées: %v", AllowedIPs)

	for {
		nConn, err := ln.Accept()
		if err != nil {
			log.Printf("[SFTP] Accept err: %v", err)
			continue
		}
		if tc, ok := nConn.(*net.TCPConn); ok {
			_ = tc.SetKeepAlive(true)
			_ = tc.SetKeepAlivePeriod(15 * time.Second)
		}

		go func(conn net.Conn) {
			sshConn, chans, reqs, err := ssh.NewServerConn(conn, cfg)
			if err != nil {
				log.Printf("[SFTP] Handshake err: %v", err)
				_ = conn.Close()
				return
			}
			log.Printf("[SFTP] Client connecté: %s (%s)", sshConn.RemoteAddr(), sshConn.ClientVersion())
			go ssh.DiscardRequests(reqs)

			for newCh := range chans {
				if newCh.ChannelType() != "session" {
					newCh.Reject(ssh.UnknownChannelType, "only session channels are allowed")
					continue
				}
				ch, reqs, err := newCh.Accept()
				if err != nil {
					log.Printf("[SFTP] Accept channel: %v", err)
					continue
				}
				go func(in <-chan *ssh.Request) {
					for req := range in {
						switch req.Type {
						case "subsystem":
							// payload = uint32(len) + "sftp"
							if len(req.Payload) >= 4 && string(req.Payload[4:]) == "sftp" {
								server, err := sftp.NewServer(ch) // API minimale
								if err != nil {
									log.Printf("[SFTP] init error: %v", err)
									_, _ = ch.Stderr().Write([]byte("sftp init error\n"))
									_ = ch.Close()
									return
								}
								_ = req.Reply(true, nil)

								if err := server.Serve(); err == io.EOF {
									_ = server.Close()
								} else if err != nil {
									log.Printf("[SFTP] serve error: %v", err)
								}
								return
							}
							_ = req.Reply(false, nil)
						default:
							_ = req.Reply(false, nil)
						}
					}
				}(reqs)
			}
		}(nConn)
	}
}


// ---------- HTTP (Gin) ----------

//go:embed web/* web/**/*
// toutes les ressources front embarquées
var webFS embed.FS

func startHTTP() {
	bd := db.InitDB()
	app := gin.Default()

	api := app.Group("/api/v1")
	routes.AddRoutes(api, bd)
	utils.CreateDefaultFolder(bd)

	// Sous-FS pointé sur "web"


	// /static -> ressources front
	app.Static("/static", "./web")
	app.GET("/", func(c *gin.Context) { c.File("./web/index.html") })
	app.NoRoute(func(c *gin.Context) {
	if strings.HasPrefix(c.Request.URL.Path, "/api/") {
		c.JSON(404, gin.H{"error":"Not found"}); return
	}
	c.File("./web/index.html")
	})
		log.Println("[HTTP] Serveur Gin sur http://0.0.0.0:8080")
		_ = app.Run(":8080")
	}


func main() {
	// SFTP sur 2222 (root = ./upload)

	// HTTP normal
	startHTTP()
	go startSFTPServer(SFTPBaseDir)

}
func loadOrCreateRSAHostKey(path string) (ssh.Signer, error) {
    if _, err := os.Stat(path); err == nil {
        b, err := os.ReadFile(path)
        if err != nil { return nil, err }
        return ssh.ParsePrivateKey(b)
    }
    // Génère une clé RSA 2048
    priv, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil { return nil, err }
    // Encode en PEM "RSA PRIVATE KEY" (PKCS#1)
    pkcs1 := x509.MarshalPKCS1PrivateKey(priv)
    pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkcs1})
    if err := os.WriteFile(path, pemBytes, 0o600); err != nil { return nil, err }
    return ssh.ParsePrivateKey(pemBytes)
}