package dicom

import (
	"bytes"
	"context"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"sync"
)

// Global port allocator for concurrent safety.
var (
	portMu       sync.Mutex
	portInUse    = make(map[int]bool)
)

// allocatePort picks a free port from the range [base, base+range).
func allocatePort(base, size int) (int, func()) {
	portMu.Lock()
	defer portMu.Unlock()

	for i := 0; i < size; i++ {
		port := base + i
		if !portInUse[port] {
			portInUse[port] = true
			return port, func() {
				portMu.Lock()
				delete(portInUse, port)
				portMu.Unlock()
			}
		}
	}
	return 0, nil
}

// StorescpResult holds the outcome of a storescp process.
type StorescpResult struct {
	PID    int
	Port   int
	Stderr string
	Err    error
}

// StartStoresCP launches storescp in the background.
// Returns a result channel that will receive the outcome when the process exits,
// plus a stop function to kill the process, and an error if startup failed.
func StartStoresCP(ctx context.Context, bin, aeTitle string, port int, outputDir string) (resultCh <-chan StorescpResult, stop func(), err error) {
	args := []string{
		strconv.Itoa(port),
		"-aet", aeTitle,
		"-od", outputDir,
		"+xa", // accept all transfer syntaxes
	}

	slog.Info("starting storescp",
		"bin", bin,
		"port", port,
		"ae", aeTitle,
		"dir", outputDir,
	)

	cmd := exec.CommandContext(ctx, bin, args...)

	// Redirect stderr to a buffer (storescp logs association info to stderr)
	var stderrBuf bytes.Buffer
	cmd.Stderr = &stderrBuf
	cmd.Stdout = nil // storescp doesn't use stdout

	if err := cmd.Start(); err != nil {
		return nil, nil, fmt.Errorf("start storescp: %w", err)
	}

	pid := cmd.Process.Pid
	ch := make(chan StorescpResult, 1)

	go func() {
		err := cmd.Wait()
		ch <- StorescpResult{
			PID:    pid,
			Port:   port,
			Stderr: stderrBuf.String(),
			Err:    err,
		}
		close(ch)
	}()

	stop = func() {
		if cmd.Process != nil {
			slog.Info("stopping storescp", "pid", pid, "port", port)
			cmd.Process.Signal(os.Interrupt) // SIGINT for clean shutdown
			go func() {
				cmd.Wait()
			}()
		}
	}

	return ch, stop, nil
}

// RunMoveSCU executes movescu for the given accession number.
// It uses Study Root query/retrieve model (via -S flag).
func RunMoveSCU(ctx context.Context, bin, ourAE, pacsAE, pacsHost string, pacsPort, moveDestPort int, accessionNumber string, timeoutSec int) (exitCode int, stdout, stderr string, err error) {
	args := []string{
		"-aet", ourAE,
		"-aec", pacsAE,
		"-aem", ourAE,
		pacsHost, strconv.Itoa(pacsPort),
		"-S", // Study Root query/retrieve
		"-k", fmt.Sprintf("0008,0050=%s", accessionNumber),
		"-k", "0010,0020=", // Patient ID wildcard (required for Study Root)
		"--no-port",
	}

	if timeoutSec > 0 {
		args = append(args, "-to", strconv.Itoa(timeoutSec))
	}

	slog.Info("running movescu",
		"bin", bin,
		"accession", accessionNumber,
		"pacs", fmt.Sprintf("%s@%s:%d", pacsAE, pacsHost, pacsPort),
		"dest", fmt.Sprintf("%s:%d", ourAE, moveDestPort),
	)

	cmd := exec.CommandContext(ctx, bin, args...)
	var outBuf, errBuf bytes.Buffer
	cmd.Stdout = &outBuf
	cmd.Stderr = &errBuf

	err = cmd.Run()
	stdout = outBuf.String()
	stderr = errBuf.String()

	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			exitCode = exitErr.ExitCode()
		} else {
			exitCode = -1
		}
		return exitCode, stdout, stderr, fmt.Errorf("movescu failed (exit %d): %s", exitCode, strings.TrimSpace(stderr))
	}

	return 0, stdout, stderr, nil
}

// RunStoreSCU sends DICOM files to a remote AE via C-STORE.
// Equivalent to: storescu -aet <ae> +sd +r <host> <port> <dir>
func RunStoreSCU(ctx context.Context, bin, aeTitle, host string, port int, dir string) (exitCode int, stdout, stderr string, err error) {
	args := []string{
		"-aet", aeTitle,
		"+sd", // scan directories
		"+r",  // recurse
		host,
		strconv.Itoa(port),
		dir,
	}

	slog.Info("running storescu",
		"bin", bin,
		"ae", aeTitle,
		"destination", fmt.Sprintf("%s:%d", host, port),
		"dir", dir,
	)

	cmd := exec.CommandContext(ctx, bin, args...)
	var outBuf, errBuf bytes.Buffer
	cmd.Stdout = &outBuf
	cmd.Stderr = &errBuf

	err = cmd.Run()
	stdout = outBuf.String()
	stderr = errBuf.String()

	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			exitCode = exitErr.ExitCode()
		} else {
			exitCode = -1
		}
		return exitCode, stdout, stderr, fmt.Errorf("storescu failed (exit %d): %s", exitCode, strings.TrimSpace(stderr))
	}

	return 0, stdout, stderr, nil
}

// CopyDir copies a directory recursively using cp -r.
func CopyDir(src, dst string) error {
	cmd := exec.Command("/bin/cp", "-r", src, dst)
	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("copy %s -> %s: %w (stderr: %s)", src, dst, err, strings.TrimSpace(stderr.String()))
	}
	return nil
}

// FileExists checks if a path exists and is a regular file.
func FileExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && !info.IsDir()
}

// DirExists checks if a path exists and is a directory.
func DirExists(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}

// CountFiles counts regular files in a directory (non-recursive).
func CountFiles(dir string) (int, error) {
	dirents, err := os.ReadDir(dir)
	if err != nil {
		return 0, fmt.Errorf("read dir %q: %w", dir, err)
	}
	count := 0
	for _, de := range dirents {
		if !de.IsDir() {
			count++
		}
	}
	return count, nil
}
