// Package docker fournit un client HTTP léger pour l'API Docker via socket Unix.
// Utilisé pour reconstruire et redémarrer le container backend lors de l'installation d'un module.
package docker

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync/atomic"
	"time"
)

// IsRebuilding est vrai lorsqu'un rebuild est en cours.
var IsRebuilding atomic.Bool

// Client communique avec le daemon Docker via le socket Unix monté en volume.
type Client struct {
	http       *http.Client
	socketPath string
}

// New crée un Client Docker. Utilise /var/run/docker.sock par défaut.
func New() *Client {
	socketPath := envOr("DOCKER_SOCKET", "/var/run/docker.sock")
	transport := &http.Transport{
		DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
			return (&net.Dialer{}).DialContext(ctx, "unix", socketPath)
		},
	}
	return &Client{
		http:       &http.Client{Transport: transport, Timeout: 15 * time.Minute},
		socketPath: socketPath,
	}
}

// Available indique si le socket Docker est accessible.
func (c *Client) Available() bool {
	_, err := os.Stat(c.socketPath)
	return err == nil
}

// ---- Structures Docker API ----

// buildEvent est une ligne du stream JSON retourné par l'API build.
type buildEvent struct {
	Stream string `json:"stream"`
	Error  string `json:"error"`
}

// inspectResult contient les champs utiles retournés par /containers/{id}/json.
type inspectResult struct {
	Name   string `json:"Name"`
	Config struct {
		Image        string              `json:"Image"`
		Env          []string            `json:"Env"`
		Cmd          []string            `json:"Cmd"`
		Entrypoint   []string            `json:"Entrypoint"`
		WorkingDir   string              `json:"WorkingDir"`
		ExposedPorts map[string]struct{} `json:"ExposedPorts"`
	} `json:"Config"`
	HostConfig struct {
		Binds         []string `json:"Binds"`
		RestartPolicy struct {
			Name string `json:"Name"`
		} `json:"RestartPolicy"`
		GroupAdd    []string `json:"GroupAdd"`
		NetworkMode string   `json:"NetworkMode"`
	} `json:"HostConfig"`
	NetworkSettings struct {
		Networks map[string]struct {
			NetworkID string `json:"NetworkID"`
		} `json:"Networks"`
	} `json:"NetworkSettings"`
}

// containerCreateBody correspond au corps de POST /containers/create.
type containerCreateBody struct {
	Image        string              `json:"Image"`
	Env          []string            `json:"Env"`
	Cmd          []string            `json:"Cmd,omitempty"`
	Entrypoint   []string            `json:"Entrypoint,omitempty"`
	WorkingDir   string              `json:"WorkingDir,omitempty"`
	ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"`
	HostConfig   struct {
		Binds         []string `json:"Binds"`
		RestartPolicy struct {
			Name string `json:"Name"`
		} `json:"RestartPolicy"`
		GroupAdd    []string `json:"GroupAdd,omitempty"`
		NetworkMode string   `json:"NetworkMode,omitempty"`
	} `json:"HostConfig"`
	NetworkingConfig struct {
		EndpointsConfig map[string]map[string]string `json:"EndpointsConfig"`
	} `json:"NetworkingConfig"`
}

// ---- Opérations Docker ----

// BuildImage construit une nouvelle image depuis le sous-répertoire d'un repo git.
// Le build arg MODULES contient les IDs de modules séparés par des virgules.
func (c *Client) BuildImage(ctx context.Context, imageTag, gitRepo, gitBranch, subdir string, moduleIDs []string) error {
	modulesStr := strings.Join(moduleIDs, ",")

	// Format Docker : repo.git#branch:sous-répertoire
	remoteCtx := fmt.Sprintf("%s#%s:%s", gitRepo, gitBranch, subdir)

	buildArgsJSON, _ := json.Marshal(map[string]string{"MODULES": modulesStr})

	q := url.Values{}
	q.Set("remote", remoteCtx)
	q.Set("dockerfile", "Dockerfile")
	q.Set("t", imageTag)
	q.Set("buildargs", string(buildArgsJSON))
	q.Set("rm", "1")

	resp, err := c.do(ctx, "POST", "/v1.47/build?"+q.Encode(), nil)
	if err != nil {
		return fmt.Errorf("appel API build : %w", err)
	}
	defer resp.Body.Close()

	// Lire le stream de build ligne par ligne — retourner dès qu'une erreur est détectée.
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		var ev buildEvent
		if err := json.Unmarshal(scanner.Bytes(), &ev); err != nil {
			continue
		}
		if ev.Error != "" {
			return fmt.Errorf("erreur build Docker : %s", ev.Error)
		}
		if ev.Stream != "" {
			log.Printf("[docker-build] %s", strings.TrimRight(ev.Stream, "\n"))
		}
	}
	return scanner.Err()
}

// InspectContainer retourne la configuration du container (pour le recréer).
func (c *Client) InspectContainer(ctx context.Context, name string) (*inspectResult, error) {
	resp, err := c.do(ctx, "GET", "/v1.47/containers/"+name+"/json", nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)

	var info inspectResult
	if err := json.Unmarshal(body, &info); err != nil {
		return nil, fmt.Errorf("parse inspect : %w", err)
	}
	return &info, nil
}

// CreateContainer crée un nouveau container et retourne son ID.
func (c *Client) CreateContainer(ctx context.Context, name string, body *containerCreateBody) (string, error) {
	bodyJSON, _ := json.Marshal(body)
	resp, err := c.do(ctx, "POST", "/v1.47/containers/create?name="+url.QueryEscape(name), bytes.NewReader(bodyJSON))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(resp.Body)

	var result struct {
		ID string `json:"Id"`
	}
	json.Unmarshal(raw, &result)
	if result.ID == "" {
		return "", fmt.Errorf("création container échouée : %s", string(raw))
	}
	return result.ID, nil
}

// StartContainer démarre un container par son ID.
func (c *Client) StartContainer(ctx context.Context, id string) error {
	_, err := c.do(ctx, "POST", "/v1.47/containers/"+id+"/start", nil)
	return err
}

// StopContainer arrête un container (timeout 15s).
func (c *Client) StopContainer(ctx context.Context, name string) error {
	_, err := c.do(ctx, "POST", "/v1.47/containers/"+name+"/stop?t=15", nil)
	return err
}

// RemoveContainer supprime un container arrêté.
func (c *Client) RemoveContainer(ctx context.Context, name string) error {
	_, err := c.do(ctx, "DELETE", "/v1.47/containers/"+name, nil)
	return err
}

// RenameContainer renomme un container.
func (c *Client) RenameContainer(ctx context.Context, current, newName string) error {
	_, err := c.do(ctx, "POST",
		fmt.Sprintf("/v1.47/containers/%s/rename?name=%s", current, url.QueryEscape(newName)), nil)
	return err
}

// ---- Orchestration rebuild ----

// RebuildAndRestart lance un rebuild en arrière-plan :
//  1. Construit la nouvelle image avec les modules demandés.
//  2. Crée un container de remplacement qui, à son démarrage, arrêtera l'ancien.
//
// Cette méthode est non-bloquante : elle retourne immédiatement.
// La réponse HTTP doit être envoyée AVANT d'appeler cette méthode.
func (c *Client) RebuildAndRestart(moduleIDs []string) {
	if !IsRebuilding.CompareAndSwap(false, true) {
		log.Println("[docker] Rebuild déjà en cours, ignoré")
		return
	}

	containerName := envOr("CONTAINER_NAME", "proxmoxpanel-backend")
	gitRepo := envOr("GIT_REPO", "https://git.geronzi.fr/proxmoxPanel/core.git")
	gitBranch := envOr("GIT_BRANCH", "frontend/alpine")
	imageTag := envOr("IMAGE_TAG", "proxmoxpanel-backend:latest")
	nextName := containerName + "-next"

	go func() {
		defer IsRebuilding.Store(false)

		log.Printf("[docker] Rebuild démarré — modules : %v", moduleIDs)

		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
		defer cancel()

		// Étape 1 — Build de la nouvelle image
		if err := c.BuildImage(ctx, imageTag, gitRepo, gitBranch, "backend", moduleIDs); err != nil {
			log.Printf("[docker] Erreur build image : %v", err)
			return
		}
		log.Println("[docker] Nouvelle image construite")

		// Étape 2 — Inspecter le container courant pour récupérer sa config
		info, err := c.InspectContainer(ctx, containerName)
		if err != nil {
			log.Printf("[docker] Erreur inspection container : %v", err)
			return
		}

		// Étape 3 — Préparer la config du container de remplacement
		body := buildReplaceBody(info, imageTag, nextName, containerName)
		bodyJSON, _ := json.Marshal(body)
		log.Printf("[docker] Config container next : %s", string(bodyJSON))

		// Supprimer le container "next" s'il existe déjà (rebuild précédent raté)
		c.StopContainer(ctx, nextName)
		c.RemoveContainer(ctx, nextName)

		// Étape 4 — Créer le container de remplacement
		nextID, err := c.CreateContainer(ctx, nextName, body)
		if err != nil {
			log.Printf("[docker] Erreur création container next : %v", err)
			return
		}

		// Étape 5 — Démarrer le container de remplacement
		// Il stoppera et remplacera l'actuel au démarrage (voir main.go : handleReplacement).
		if err := c.StartContainer(ctx, nextID); err != nil {
			log.Printf("[docker] Erreur démarrage container next : %v", err)
			return
		}

		log.Printf("[docker] Container %s démarré — remplacement de %s en cours", nextName, containerName)
	}()
}

// Restart redémarre le container courant en arrière-plan (même image).
// Utilisé lors de l'activation/désactivation d'un module has_backend.
func (c *Client) Restart() {
	containerName := envOr("CONTAINER_NAME", "proxmoxpanel-backend")
	go func() {
		time.Sleep(500 * time.Millisecond)
		log.Printf("[docker] Redémarrage du container %s...", containerName)
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()
		if err := c.StopContainer(ctx, containerName); err != nil {
			log.Printf("[docker] Erreur stop container : %v", err)
		}
		// La restart policy unless-stopped ne redémarre pas après un stop explicite.
		// On recrée le container à partir de son inspect.
		info, err := c.InspectContainer(ctx, containerName)
		if err != nil {
			log.Printf("[docker] Erreur inspect pour restart : %v", err)
			return
		}
		c.RemoveContainer(ctx, containerName)
		body := buildSameBody(info)
		id, err := c.CreateContainer(ctx, containerName, body)
		if err != nil {
			log.Printf("[docker] Erreur recréation container : %v", err)
			return
		}
		if err := c.StartContainer(ctx, id); err != nil {
			log.Printf("[docker] Erreur démarrage après restart : %v", err)
		}
	}()
}

// HandleReplacement gère le cas où ce container est un remplacement.
// À appeler au tout début de main() si REPLACING_CONTAINER est défini.
// Arrête l'ancien container, le supprime, se renomme.
func (c *Client) HandleReplacement() {
	old := os.Getenv("REPLACING_CONTAINER")
	myName := envOr("CONTAINER_NAME", "proxmoxpanel-backend-next")
	if old == "" || !c.Available() {
		return
	}

	log.Printf("[docker] Mode remplacement : arrêt du container %s...", old)
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Petit délai pour que l'ancien container finisse d'envoyer ses réponses HTTP en cours
	time.Sleep(2 * time.Second)

	if err := c.StopContainer(ctx, old); err != nil {
		log.Printf("[docker] Avertissement : impossible d'arrêter %s : %v", old, err)
	}
	if err := c.RemoveContainer(ctx, old); err != nil {
		log.Printf("[docker] Avertissement : impossible de supprimer %s : %v", old, err)
	}

	// Se renommer pour prendre l'identité du container remplacé
	if err := c.RenameContainer(ctx, myName, old); err != nil {
		log.Printf("[docker] Avertissement : impossible de renommer %s → %s : %v", myName, old, err)
	} else {
		log.Printf("[docker] Container renommé %s → %s", myName, old)
	}
}

// ---- Helpers internes ----

// buildReplaceBody construit la config du container de remplacement.
// Ajoute REPLACING_CONTAINER et met à jour CONTAINER_NAME dans les env vars.
func buildReplaceBody(info *inspectResult, newImage, newContainerName, replacingName string) *containerCreateBody {
	body := buildSameBody(info)
	body.Image = newImage

	// Mettre à jour les env vars de remplacement
	newEnv := make([]string, 0, len(info.Config.Env)+2)
	for _, e := range info.Config.Env {
		if strings.HasPrefix(e, "CONTAINER_NAME=") || strings.HasPrefix(e, "REPLACING_CONTAINER=") {
			continue
		}
		newEnv = append(newEnv, e)
	}
	newEnv = append(newEnv,
		"CONTAINER_NAME="+newContainerName,
		"REPLACING_CONTAINER="+replacingName,
	)
	body.Env = newEnv
	return body
}

// buildSameBody construit la config d'un container identique (même image).
func buildSameBody(info *inspectResult) *containerCreateBody {
	body := &containerCreateBody{}
	body.Image = info.Config.Image
	body.Env = info.Config.Env
	body.Cmd = info.Config.Cmd
	body.Entrypoint = info.Config.Entrypoint
	body.WorkingDir = info.Config.WorkingDir
	body.ExposedPorts = info.Config.ExposedPorts
	body.HostConfig.Binds = info.HostConfig.Binds
	body.HostConfig.RestartPolicy.Name = info.HostConfig.RestartPolicy.Name
	body.HostConfig.GroupAdd = info.HostConfig.GroupAdd
	body.HostConfig.NetworkMode = info.HostConfig.NetworkMode

	// Réseaux
	endpoints := make(map[string]map[string]string)
	for netName, netInfo := range info.NetworkSettings.Networks {
		endpoints[netName] = map[string]string{"NetworkID": netInfo.NetworkID}
	}
	body.NetworkingConfig.EndpointsConfig = endpoints
	return body
}

// do effectue une requête HTTP vers l'API Docker (http://docker = socket Unix).
func (c *Client) do(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
	req, err := http.NewRequestWithContext(ctx, method, "http://docker"+path, body)
	if err != nil {
		return nil, err
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	// Les codes 2xx et 304 sont considérés comme succès
	if resp.StatusCode >= 300 && resp.StatusCode != 304 {
		raw, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		return nil, fmt.Errorf("Docker API %s %s → HTTP %d : %s", method, path, resp.StatusCode, string(raw))
	}
	return resp, nil
}

func envOr(key, def string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return def
}
