package service

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"mkiso-server/internal/config"
	"mkiso-server/internal/isobuilder"
	"mkiso-server/internal/repo"
	"mkiso-server/pkg/dicom"
)

// ISOService handles ISO creation from DICOM data.
type ISOService struct {
	cfg        *config.Config
	dicomSvc   *DicomService
	patientRepo *repo.PatientRepo
}

// NewISOService creates a new ISOService.
func NewISOService(cfg *config.Config, dicomSvc *DicomService, patientRepo *repo.PatientRepo) *ISOService {
	return &ISOService{
		cfg:         cfg,
		dicomSvc:    dicomSvc,
		patientRepo: patientRepo,
	}
}

// ISOItem is the result of ISO generation.
type ISOItem struct {
	Path     string // Full path to the generated ISO file
	Cleanup  func() // Call to remove temp files
	Filename string // Suggested download filename
}

// GenerateISO creates an ISO for a single accession number.
func (s *ISOService) GenerateISO(ctx context.Context, accessionNumber string) (*ISOItem, error) {
	// Create temp working directory
	tempDir, err := os.MkdirTemp(s.cfg.ISO.TempDir, "dicomdir_")
	if err != nil {
		return nil, fmt.Errorf("create temp dir: %w", err)
	}
	cleanup := func() { os.RemoveAll(tempDir) }

	dicomDir := filepath.Join(tempDir, "DICOMDIR")
	if err := os.MkdirAll(dicomDir, 0755); err != nil {
		cleanup()
		return nil, fmt.Errorf("create DICOMDIR: %w", err)
	}

	// Copy microdicom viewer files
	if err := s.copyMicrodicom(tempDir); err != nil {
		cleanup()
		return nil, fmt.Errorf("copy microdicom: %w", err)
	}

	// Fetch DICOM from PACS
	_, err = s.dicomSvc.FetchDICOM(ctx, accessionNumber, dicomDir)
	if err != nil {
		cleanup()
		return nil, fmt.Errorf("DICOM fetch failed: %w", err)
	}

	// Generate ISO
	isoName := sanitizeFilename(accessionNumber) + ".iso"
	isoPath := filepath.Join(s.cfg.ISO.TempDir, isoName)

	if err := isobuilder.BuildFromDirectory(tempDir, isoPath, "DICOM"); err != nil {
		cleanup()
		os.Remove(isoPath)
		return nil, fmt.Errorf("ISO creation failed: %w", err)
	}

	slog.Info("ISO created",
		"path", isoPath,
		"accession", accessionNumber,
	)

	return &ISOItem{
		Path:     isoPath,
		Cleanup:  func() { cleanup(); os.Remove(isoPath) },
		Filename: isoName,
	}, nil
}

// GenerateISOMultiple creates an ISO for multiple accession numbers.
// It tries to get the patient name from the patient API for a descriptive filename.
// If the patient API is unavailable, it falls back to the accession list as filename.
func (s *ISOService) GenerateISOMultiple(ctx context.Context, accessionNumbers []string) (*ISOItem, error) {
	// Create temp working directory
	tempDir, err := os.MkdirTemp(s.cfg.ISO.TempDir, "dicomdir_")
	if err != nil {
		return nil, fmt.Errorf("create temp dir: %w", err)
	}
	cleanup := func() { os.RemoveAll(tempDir) }

	dicomDir := filepath.Join(tempDir, "DICOMDIR")
	if err := os.MkdirAll(dicomDir, 0755); err != nil {
		cleanup()
		return nil, fmt.Errorf("create DICOMDIR: %w", err)
	}

	// Copy microdicom viewer files
	if err := s.copyMicrodicom(tempDir); err != nil {
		cleanup()
		return nil, fmt.Errorf("copy microdicom: %w", err)
	}

	// Fetch DICOM from PACS
	_, err = s.dicomSvc.FetchDICOMMultiple(ctx, accessionNumbers, dicomDir)
	if err != nil {
		cleanup()
		return nil, fmt.Errorf("DICOM fetch failed: %w", err)
	}

	// Determine filename: try patient API, fall back to accession list
	filename := buildMultiFilename(accessionNumbers)

	patient, err := s.patientRepo.ByAccessionNumber(ctx, accessionNumbers[0])
	if err != nil {
		slog.Warn("patient API lookup failed, using accession-based filename",
			"accession", accessionNumbers[0],
			"error", err,
		)
	} else if patient != nil && patient.PatientName != "" {
		name := sanitizeFilename(patient.PatientName)
		accPart := sanitizeFilename(accessionList(accessionNumbers))
		filename = fmt.Sprintf("%s-%s.iso", name, accPart)
		slog.Info("using patient name for ISO filename",
			"patient", patient.PatientName,
			"filename", filename,
		)
	}

	isoPath := filepath.Join(s.cfg.ISO.TempDir, filename)

	if err := isobuilder.BuildFromDirectory(tempDir, isoPath, "DICOM"); err != nil {
		cleanup()
		os.Remove(isoPath)
		return nil, fmt.Errorf("ISO creation failed: %w", err)
	}

	slog.Info("ISO created (multiple)",
		"path", isoPath,
		"accessions", accessionNumbers,
	)

	return &ISOItem{
		Path:     isoPath,
		Cleanup:  func() { cleanup(); os.Remove(isoPath) },
		Filename: filename,
	}, nil
}

// copyMicrodicom copies the microdicom viewer files into the temp directory.
func (s *ISOService) copyMicrodicom(destDir string) error {
	if !dicom.DirExists(s.cfg.ISO.MicrodicomPath) {
		slog.Warn("microdicom path does not exist, skipping copy",
			"path", s.cfg.ISO.MicrodicomPath,
		)
		return nil
	}
	return dicom.CopyDir(s.cfg.ISO.MicrodicomPath, destDir)
}

// sanitizeFilename removes characters not safe for filenames.
func sanitizeFilename(s string) string {
	reg := regexp.MustCompile(`[^a-zA-Z0-9\-\.]+`)
	return strings.TrimRight(reg.ReplaceAllString(strings.ToUpper(s), ""), ".-")
}

// accessionList joins accession numbers with "-".
func accessionList(accs []string) string {
	return strings.Join(accs, "-")
}

// buildMultiFilename creates a default filename from accession numbers.
func buildMultiFilename(accs []string) string {
	return sanitizeFilename(accessionList(accs)) + ".iso"
}
