// Package iso provides utilities for reading ISO 9660 image files.
package iso

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/kdomanski/iso9660"
)

// Entry represents a file or directory inside an ISO image.
type Entry struct {
	Name    string
	Path    string    // Full path within the ISO (forward-slash separated)
	IsDir   bool
	Size    int64
	ModTime time.Time
}

// Reader wraps an ISO image file.
type Reader struct {
	path  string
	image *iso9660.Image
	file  *os.File
}

// Open opens an ISO file for reading. Call Close() when done.
func Open(isoPath string) (*Reader, error) {
	f, err := os.Open(isoPath)
	if err != nil {
		return nil, fmt.Errorf("opening iso: %w", err)
	}

	img, err := iso9660.OpenImage(f)
	if err != nil {
		f.Close()
		return nil, fmt.Errorf("parsing iso: %w", err)
	}

	return &Reader{path: isoPath, image: img, file: f}, nil
}

// Close releases the underlying file handle.
func (r *Reader) Close() error {
	return r.file.Close()
}

// ListDir returns the entries in the given directory path within the ISO.
// Use an empty string or "/" for the root directory.
func (r *Reader) ListDir(dirPath string) ([]Entry, error) {
	dirPath = cleanPath(dirPath)

	root, err := r.image.RootDir()
	if err != nil {
		return nil, fmt.Errorf("reading root: %w", err)
	}

	// Navigate to the target directory.
	dir, err := navigate(root, dirPath)
	if err != nil {
		return nil, err
	}

	children, err := dir.GetChildren()
	if err != nil {
		return nil, fmt.Errorf("listing directory: %w", err)
	}

	entries := make([]Entry, 0, len(children))
	for _, child := range children {
		name := child.Name()
		// Skip the "." and ".." entries that some ISOs include.
		if name == "." || name == ".." || name == "" {
			continue
		}

		var entryPath string
		if dirPath == "" {
			entryPath = name
		} else {
			entryPath = dirPath + "/" + name
		}

		entries = append(entries, Entry{
			Name:    name,
			Path:    entryPath,
			IsDir:   child.IsDir(),
			Size:    child.Size(),
			ModTime: child.ModTime(),
		})
	}

	return entries, nil
}

// FileReader opens a file inside the ISO for streaming.
// The caller is responsible for closing the returned ReadCloser.
func (r *Reader) FileReader(filePath string) (io.ReadCloser, int64, error) {
	filePath = cleanPath(filePath)
	if filePath == "" {
		return nil, 0, fmt.Errorf("no file path specified")
	}

	root, err := r.image.RootDir()
	if err != nil {
		return nil, 0, fmt.Errorf("reading root: %w", err)
	}

	// Split path into directory + filename.
	dir := filepath.Dir(filePath)
	name := filepath.Base(filePath)

	var parent *iso9660.File
	if dir == "." || dir == "/" {
		parent = root
	} else {
		parent, err = navigate(root, cleanPath(dir))
		if err != nil {
			return nil, 0, err
		}
	}

	children, err := parent.GetChildren()
	if err != nil {
		return nil, 0, fmt.Errorf("listing parent dir: %w", err)
	}

	for _, child := range children {
		if strings.EqualFold(child.Name(), name) {
			if child.IsDir() {
				return nil, 0, fmt.Errorf("%q is a directory", filePath)
			}
			r := child.Reader()
			if r == nil {
			    return nil, 0, fmt.Errorf("could not open reader for %q", filePath)
			}
			return io.NopCloser(r), child.Size(), nil
		}
	}

	return nil, 0, fmt.Errorf("file not found: %q", filePath)
}

// navigate traverses the directory tree inside an ISO to find the
// directory at cleanedPath (e.g. "foo/bar/baz").
func navigate(root *iso9660.File, cleanedPath string) (*iso9660.File, error) {
	if cleanedPath == "" {
		return root, nil
	}

	parts := strings.Split(cleanedPath, "/")
	current := root

	for _, part := range parts {
		if part == "" {
			continue
		}
		children, err := current.GetChildren()
		if err != nil {
			return nil, fmt.Errorf("listing dir: %w", err)
		}
		found := false
		for _, child := range children {
			if strings.EqualFold(child.Name(), part) {
				if !child.IsDir() {
					return nil, fmt.Errorf("%q is not a directory", part)
				}
				current = child
				found = true
				break
			}
		}
		if !found {
			return nil, fmt.Errorf("directory not found: %q", part)
		}
	}

	return current, nil
}

// cleanPath normalises a path: strips leading slash and cleans traversal.
func cleanPath(p string) string {
	p = filepath.ToSlash(filepath.Clean("/" + p))
	p = strings.TrimPrefix(p, "/")
	return p
}
