package storage

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"strings"
	"sync"

	"nadir/internal/mounts"
	"nadir/internal/oscmd"

	"github.com/danielgtaylor/huma/v2"
)

// fstabFile is a var so tests can point it at a fixture.
var fstabFile = "/etc/fstab"

// fstabMu serialises writes to /etc/fstab so concurrent HTTP requests don't
// clobber each other's read-modify-write.
var fstabMu sync.Mutex

// FstabEntry is one /etc/fstab line. Dump and Pass are the last two numeric
// fields (fs_freq and fs_passno).
type FstabEntry struct {
	Device     string `json:"device" example:"UUID=1234-5678"`
	Mountpoint string `json:"mountpoint" example:"/mnt/data"`
	FSType     string `json:"fstype" example:"ext4"`
	Options    string `json:"options" example:"defaults"`
	Dump       int    `json:"dump" example:"0"`
	Pass       int    `json:"pass" example:"2" doc:"fsck order (0 = skip)"`
}

type ListMountsOutput struct {
	Body struct {
		Mounts []mounts.Mount `json:"mounts"`
	}
}

type ListFstabOutput struct {
	Body struct {
		Entries []FstabEntry `json:"entries"`
	}
}

type MountInput struct {
	Body struct {
		Device     string `json:"device" example:"/dev/sdb1" doc:"Block device or UUID=/LABEL= specifier"`
		Mountpoint string `json:"mountpoint" example:"/mnt/data" doc:"Absolute mount path"`
		FSType     string `json:"fstype" example:"ext4"`
		Options    string `json:"options,omitempty" example:"defaults" doc:"Mount options (default: defaults)"`
		Dump       int    `json:"dump,omitempty"`
		Pass       int    `json:"pass,omitempty" doc:"fsck order (default 2 for real filesystems, 0 to skip)"`
	}
}

type UnmountInput struct {
	Mountpoint string `query:"mountpoint" example:"/mnt/data" doc:"Mountpoint to unmount and remove from fstab"`
}

func registerStorage(api huma.API) {
	huma.Register(api, huma.Operation{
		OperationID: "storage-list-mounts",
		Method:      "GET",
		Path:        "/api/storage/mounts",
		Summary:     "List active mounts",
		Description: "Returns the kernel mount table (/proc/mounts).",
		Tags:        []string{tagStorage},
		Metadata:    op("read"),
		Errors:      readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListMountsOutput, error) {
		entries, err := mounts.Proc()
		if err != nil {
			return nil, huma.Error500InternalServerError("mount table lookup failed", err)
		}
		res := &ListMountsOutput{}
		res.Body.Mounts = entries
		return res, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "storage-list-fstab",
		Method:      "GET",
		Path:        "/api/storage/fstab",
		Summary:     "List /etc/fstab entries",
		Description: "Returns the persistent mount definitions from /etc/fstab.",
		Tags:        []string{tagStorage},
		Metadata:    op("read"),
		Errors:      readErrors,
	}, func(ctx context.Context, _ *struct{}) (*ListFstabOutput, error) {
		data, err := os.ReadFile(fstabFile)
		if err != nil {
			return nil, huma.Error500InternalServerError("fstab lookup failed", err)
		}
		res := &ListFstabOutput{}
		res.Body.Entries = parseFstab(string(data))
		return res, nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "storage-add-mount",
		Method:      "POST",
		Path:        "/api/storage/mounts",
		Summary:     "Add and mount a filesystem",
		Description: "Appends an /etc/fstab entry and mounts it. If the mount fails the " +
			"fstab entry is rolled back, so a bad request leaves the system unchanged.",
		Tags:     []string{tagStorage},
		Metadata: op("write"),
		Errors:   writeErrors,
	}, func(ctx context.Context, in *MountInput) (*oscmd.StatusOutput, error) {
		e := FstabEntry{
			Device:     in.Body.Device,
			Mountpoint: in.Body.Mountpoint,
			FSType:     in.Body.FSType,
			Options:    in.Body.Options,
			Dump:       in.Body.Dump,
			Pass:       in.Body.Pass,
		}
		if e.Options == "" {
			e.Options = "defaults"
		}
		if err := validateEntry(e); err != nil {
			return nil, err
		}

		existing, err := readFstab()
		if err != nil {
			return nil, huma.Error500InternalServerError("fstab lookup failed", err)
		}
		if findEntry(existing, e.Mountpoint) != nil {
			return nil, huma.Error409Conflict("an fstab entry already exists for " + e.Mountpoint)
		}

		if err := appendFstabLine(e); err != nil {
			return nil, huma.Error500InternalServerError("write fstab failed", err)
		}
		// mount reads the fstab line we just wrote. On failure, roll the line back
		// so a bad device/options doesn't linger in fstab and break the next boot.
		if _, err := oscmd.RunContext(ctx, "mount", "--", e.Mountpoint); err != nil {
			if _, werr := removeFstabLines(e.Mountpoint); werr != nil {
				return nil, huma.Error500InternalServerError("mount failed and fstab rollback failed", werr)
			}
			return nil, huma.Error400BadRequest("mount failed: " + err.Error())
		}
		return oscmd.OK(), nil
	})

	huma.Register(api, huma.Operation{
		OperationID: "storage-remove-mount",
		Method:      "DELETE",
		Path:        "/api/storage/mounts",
		Summary:     "Unmount and remove a filesystem",
		Description: "Unmounts the filesystem (if mounted) and removes its /etc/fstab entry. " +
			"The mountpoint is passed as a query parameter since it contains slashes.",
		Tags:     []string{tagStorage},
		Metadata: op("write"),
		Errors:   []int{400, 401, 403, 404, 409, 500},
	}, func(ctx context.Context, in *UnmountInput) (*oscmd.StatusOutput, error) {
		if err := validateMountpoint(in.Mountpoint); err != nil {
			return nil, err
		}

		entries, err := readFstab()
		if err != nil {
			return nil, huma.Error500InternalServerError("fstab lookup failed", err)
		}
		inFstab := findEntry(entries, in.Mountpoint) != nil

		mounted, err := isMounted(in.Mountpoint)
		if err != nil {
			return nil, huma.Error500InternalServerError("mount table lookup failed", err)
		}
		if !inFstab && !mounted {
			return nil, huma.Error404NotFound("no mount or fstab entry for " + in.Mountpoint)
		}

		if mounted {
			if _, err := oscmd.RunContext(ctx, "umount", "--", in.Mountpoint); err != nil {
				return nil, huma.Error409Conflict("unmount failed (in use?): " + err.Error())
			}
		}
		if inFstab {
			if _, err := removeFstabLines(in.Mountpoint); err != nil {
				return nil, huma.Error500InternalServerError("write fstab failed", err)
			}
		}
		return oscmd.OK(), nil
	})
}

// --- fstab helpers -----------------------------------------------------------

func readFstab() ([]FstabEntry, error) {
	data, err := os.ReadFile(fstabFile)
	if err != nil {
		return nil, err
	}
	return parseFstab(string(data)), nil
}

// parseFstab parses /etc/fstab, skipping comments and blank lines.
func parseFstab(data string) []FstabEntry {
	entries := []FstabEntry{}
	for line := range strings.SplitSeq(data, "\n") {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		f := strings.Fields(trimmed)
		if len(f) < 4 { // device mountpoint fstype options [dump] [pass]
			continue
		}
		e := FstabEntry{
			Device:     mounts.Unescape(f[0]),
			Mountpoint: mounts.Unescape(f[1]),
			FSType:     f[2],
			Options:    f[3],
		}
		if len(f) >= 5 {
			e.Dump, _ = strconv.Atoi(f[4])
		}
		if len(f) >= 6 {
			e.Pass, _ = strconv.Atoi(f[5])
		}
		entries = append(entries, e)
	}
	return entries
}

// fstab edits are surgical (append one line / drop matching lines) rather than a
// whole-file rewrite, so an admin's existing entries, comments and formatting in
// this boot-critical file are preserved — same approach as /etc/hosts.

func renderFstabLine(e FstabEntry) string {
	return fmt.Sprintf("%s\t%s\t%s\t%s\t%d\t%d", e.Device, e.Mountpoint, e.FSType, e.Options, e.Dump, e.Pass)
}

// writeFstabAtomically writes content to /etc/fstab using a temporary file and
// rename, so a crash mid-write leaves the original file intact.
func writeFstabAtomically(content string) error {
	tmp := fstabFile + ".nadir.tmp"
	if err := os.WriteFile(tmp, []byte(content), 0644); err != nil {
		return err
	}
	if err := os.Rename(tmp, fstabFile); err != nil {
		os.Remove(tmp)
		return err
	}
	return nil
}

// appendFstabLine adds one entry, leaving every existing line untouched.
func appendFstabLine(e FstabEntry) error {
	fstabMu.Lock()
	defer fstabMu.Unlock()

	data, err := os.ReadFile(fstabFile)
	if err != nil {
		return err
	}
	content := string(data)
	if content != "" && !strings.HasSuffix(content, "\n") {
		content += "\n"
	}
	content += renderFstabLine(e) + "\n"
	return writeFstabAtomically(content)
}

// removeFstabLines drops every line mapping mountpoint, preserving comments and
// other entries. Reports whether anything was removed.
func removeFstabLines(mountpoint string) (bool, error) {
	fstabMu.Lock()
	defer fstabMu.Unlock()

	data, err := os.ReadFile(fstabFile)
	if err != nil {
		return false, err
	}
	lines := strings.Split(string(data), "\n")
	kept := make([]string, 0, len(lines))
	removed := false
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed != "" && !strings.HasPrefix(trimmed, "#") {
			if f := strings.Fields(trimmed); len(f) >= 2 && mounts.Unescape(f[1]) == mountpoint {
				removed = true
				continue
			}
		}
		kept = append(kept, line)
	}
	if !removed {
		return false, nil
	}
	return true, writeFstabAtomically(strings.Join(kept, "\n"))
}

func findEntry(entries []FstabEntry, mountpoint string) *FstabEntry {
	for i := range entries {
		if entries[i].Mountpoint == mountpoint {
			return &entries[i]
		}
	}
	return nil
}

// isMounted reports whether mountpoint currently appears in /proc/mounts.
func isMounted(mountpoint string) (bool, error) {
	active, err := mounts.Proc()
	if err != nil {
		return false, err
	}
	for _, e := range active {
		if e.Mountpoint == mountpoint {
			return true, nil
		}
	}
	return false, nil
}

func validateEntry(e FstabEntry) error {
	if !deviceRe.MatchString(e.Device) {
		return huma.Error400BadRequest("invalid device: " + e.Device)
	}
	if err := validateMountpoint(e.Mountpoint); err != nil {
		return err
	}
	if !fstypeRe.MatchString(e.FSType) {
		return huma.Error400BadRequest("invalid fstype: " + e.FSType)
	}
	if !optionsRe.MatchString(e.Options) {
		return huma.Error400BadRequest("invalid options: " + e.Options)
	}
	return nil
}
