package webhook

import (
	"context"
	"crypto/subtle"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"

	"github.com/alexei/docker-watcher/internal/docker"
	"github.com/alexei/docker-watcher/internal/store"
)

// DeployTriggerer is called when a webhook determines a deploy should happen.
// Same interface as registry.DeployTriggerer — kept separate to avoid import cycles.
type DeployTriggerer interface {
	TriggerDeploy(ctx context.Context, projectID, stageID, imageTag string) error
}

// ImageInspector abstracts Docker image inspection for testability.
type ImageInspector interface {
	InspectImage(ctx context.Context, imageRef string) (docker.ImageInfo, error)
}

// Payload is the expected JSON body for a webhook request.
type Payload struct {
	// Image is the full image reference including tag, e.g.
	// "git.dolgolyov-family.by/alexei/web-app-launcher:dev-abc123".
	Image string `json:"image"`
}

// ParsedImage holds the components extracted from a full image reference string.
type ParsedImage struct {
	// Registry is the hostname, e.g. "git.dolgolyov-family.by".
	Registry string
	// Owner is the namespace/org, e.g. "alexei".
	Owner string
	// Name is the repository name, e.g. "web-app-launcher".
	Name string
	// Tag is the image tag, e.g. "dev-abc123". Empty string means "latest".
	Tag string
}

// FullName returns "owner/name" (the image path without registry and tag).
func (p ParsedImage) FullName() string {
	if p.Owner != "" {
		return p.Owner + "/" + p.Name
	}
	return p.Name
}

// ParseImageRef splits a full image reference into its components.
// Accepted formats:
//
//	registry.example.com/owner/name:tag
//	registry.example.com/owner/name
//	owner/name:tag
//	name:tag
func ParseImageRef(ref string) (ParsedImage, error) {
	ref = strings.TrimSpace(ref)
	if ref == "" {
		return ParsedImage{}, fmt.Errorf("empty image reference")
	}

	var parsed ParsedImage

	// Split off tag.
	if idx := strings.LastIndex(ref, ":"); idx != -1 {
		// Make sure the colon is not inside the registry host (e.g. "localhost:5000/img").
		afterColon := ref[idx+1:]
		if !strings.Contains(afterColon, "/") {
			parsed.Tag = afterColon
			ref = ref[:idx]
		}
	}

	parts := strings.Split(ref, "/")
	switch len(parts) {
	case 1:
		// "name"
		parsed.Name = parts[0]
	case 2:
		// "owner/name"
		parsed.Owner = parts[0]
		parsed.Name = parts[1]
	default:
		// "registry/owner/name" or "registry/owner/sub/name" — first segment is registry.
		parsed.Registry = parts[0]
		parsed.Owner = strings.Join(parts[1:len(parts)-1], "/")
		parsed.Name = parts[len(parts)-1]
	}

	if parsed.Name == "" {
		return ParsedImage{}, fmt.Errorf("invalid image reference: missing name in %q", ref)
	}

	return parsed, nil
}

// Handler is the HTTP handler for webhook requests.
type Handler struct {
	store     *store.Store
	deployer  DeployTriggerer
	inspector ImageInspector
}

// NewHandler creates a new webhook Handler.
func NewHandler(st *store.Store, deployer DeployTriggerer, inspector ImageInspector) *Handler {
	return &Handler{
		store:     st,
		deployer:  deployer,
		inspector: inspector,
	}
}

// Route returns a chi router with the webhook endpoint mounted.
func (h *Handler) Route() chi.Router {
	r := chi.NewRouter()
	r.Post("/{secret}", h.handleWebhook)
	return r
}

// handleWebhook processes an incoming webhook request.
// URL format: POST /api/webhook/{secret-uuid}
// Returns 404 for invalid secrets (no information leak).
func (h *Handler) handleWebhook(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	secret := chi.URLParam(r, "secret")
	if secret == "" {
		http.NotFound(w, r)
		return
	}

	// Validate the webhook secret against stored settings.
	settings, err := h.store.GetSettings()
	if err != nil {
		log.Printf("[webhook] failed to read settings: %v", err)
		http.NotFound(w, r)
		return
	}

	if settings.WebhookSecret == "" || subtle.ConstantTimeCompare([]byte(settings.WebhookSecret), []byte(secret)) != 1 {
		http.NotFound(w, r)
		return
	}

	// Parse the request body.
	var payload Payload
	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
		http.Error(w, `{"error":"invalid JSON payload"}`, http.StatusBadRequest)
		return
	}

	if payload.Image == "" {
		http.Error(w, `{"error":"missing image field"}`, http.StatusBadRequest)
		return
	}

	parsed, err := ParseImageRef(payload.Image)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
		return
	}

	// Default tag to "latest" if omitted.
	if parsed.Tag == "" {
		parsed.Tag = "latest"
	}

	log.Printf("[webhook] received push for image %s:%s", parsed.FullName(), parsed.Tag)

	// Look up a matching project by image name.
	project, stage, found, err := FindProjectAndStage(ctx, h.store, parsed)
	if err != nil {
		log.Printf("[webhook] lookup error: %v", err)
		http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
		return
	}

	if !found {
		// Unknown project — auto-create with defaults from image inspection.
		log.Printf("[webhook] unknown image %s, auto-creating project", parsed.FullName())
		project, stage, err = AutoCreateProject(ctx, h.store, h.inspector, parsed)
		if err != nil {
			log.Printf("[webhook] auto-create failed: %v", err)
			http.Error(w, `{"error":"failed to auto-create project"}`, http.StatusInternalServerError)
			return
		}
		log.Printf("[webhook] auto-created project %s (%s) with stage %s", project.Name, project.ID, stage.Name)
	}

	// Only deploy if auto_deploy is enabled for the matched stage.
	if !stage.AutoDeploy {
		log.Printf("[webhook] auto_deploy disabled for project %s stage %s, skipping deploy", project.Name, stage.Name)
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(map[string]any{"status": "accepted", "deploy": false, "project": project.Name, "stage": stage.Name})
		return
	}

	if err := h.deployer.TriggerDeploy(ctx, project.ID, stage.ID, parsed.Tag); err != nil {
		log.Printf("[webhook] deploy trigger failed: %v", err)
		http.Error(w, `{"error":"deploy trigger failed"}`, http.StatusInternalServerError)
		return
	}

	log.Printf("[webhook] triggered deploy for project %s stage %s tag %s", project.Name, stage.Name, parsed.Tag)
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]any{"status": "accepted", "deploy": true, "project": project.Name, "stage": stage.Name, "tag": parsed.Tag})
}

// EnsureWebhookSecret checks whether a webhook secret exists in settings.
// If not, it generates a new UUID and stores it. Returns the current secret.
func EnsureWebhookSecret(st *store.Store) (string, error) {
	settings, err := st.GetSettings()
	if err != nil {
		return "", fmt.Errorf("get settings: %w", err)
	}

	if settings.WebhookSecret != "" {
		return settings.WebhookSecret, nil
	}

	settings.WebhookSecret = uuid.New().String()
	if err := st.UpdateSettings(settings); err != nil {
		return "", fmt.Errorf("store webhook secret: %w", err)
	}

	log.Printf("[webhook] generated new webhook secret")
	return settings.WebhookSecret, nil
}

// RegenerateWebhookSecret generates a new webhook secret UUID, replacing and
// invalidating the old one. Returns the new secret.
func RegenerateWebhookSecret(st *store.Store) (string, error) {
	settings, err := st.GetSettings()
	if err != nil {
		return "", fmt.Errorf("get settings: %w", err)
	}

	settings.WebhookSecret = uuid.New().String()
	if err := st.UpdateSettings(settings); err != nil {
		return "", fmt.Errorf("store webhook secret: %w", err)
	}

	log.Printf("[webhook] regenerated webhook secret")
	return settings.WebhookSecret, nil
}
