package zddc

import (
	"errors"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
)

// Virtual project-level table views — SSR, MDL rollup, RSK rollup.
//
// All three are declared `virtual: true` in defaults.zddc.yaml under
// `<project>/{ssr,mdl,rsk}`. The folder does not exist on disk: the
// server synthesizes listings by walking archive/*/ at request time
// and rewrites file reads/writes back to canonical paths inside the
// per-party folders. ACL on each synthetic row is evaluated against
// the canonical `<project>/archive/<party>/` chain, so party owners
// can edit their own rows and non-owners see them read-only.
//
// URL conventions
//
//	/<project>/ssr/                              → directory listing
//	/<project>/ssr/table.yaml | form.yaml        → embedded default spec
//	/<project>/ssr/<party>.yaml                  → reads <project>/archive/<party>/ssr.yaml
//	/<project>/ssr/<party>.yaml.html             → form edit (form recognizer)
//	/<project>/ssr/form.html                     → "+ Add row" — SSR create
//
//	/<project>/mdl/                              → rollup listing
//	/<project>/mdl/table.yaml | form.yaml        → embedded default project-rollup spec
//	/<project>/mdl/<party>__<file>.yaml          → reads <project>/archive/<party>/mdl/<file>.yaml
//	/<project>/mdl/<party>__<file>.yaml.html     → form edit
//
//	/<project>/rsk/                              → analogous
//
// Modeled on virtualreceived.go: one resolver produces canonical
// paths; every caller (listing builder, file API rewrite, form
// recognizer) reads its policy chain from the canonical path.

// VirtualViewKind classifies a resolved virtual URL.
type VirtualViewKind int

const (
	VirtualViewNone VirtualViewKind = iota
	VirtualViewSSRRoot
	VirtualViewSSRSpec
	VirtualViewSSRRow
	VirtualViewMDLRoot
	VirtualViewMDLSpec
	VirtualViewMDLRow
	VirtualViewRSKRoot
	VirtualViewRSKSpec
	VirtualViewRSKRow
)

// IsRowKind reports whether k targets a per-party row file (true for
// SSRRow, MDLRow, RSKRow).
func (k VirtualViewKind) IsRowKind() bool {
	switch k {
	case VirtualViewSSRRow, VirtualViewMDLRow, VirtualViewRSKRow:
		return true
	}
	return false
}

// IsSpecKind reports whether k targets a virtual table.yaml/form.yaml.
func (k VirtualViewKind) IsSpecKind() bool {
	switch k {
	case VirtualViewSSRSpec, VirtualViewMDLSpec, VirtualViewRSKSpec:
		return true
	}
	return false
}

// IsRootKind reports whether k targets the listing-level URL of a
// virtual view.
func (k VirtualViewKind) IsRootKind() bool {
	switch k {
	case VirtualViewSSRRoot, VirtualViewMDLRoot, VirtualViewRSKRoot:
		return true
	}
	return false
}

// VirtualViewResolution captures the result of mapping a URL onto
// one of the project-level virtual table views. All fields are
// populated only when Resolved is true.
type VirtualViewResolution struct {
	Resolved bool
	Kind     VirtualViewKind

	Project    string // "<project>"
	ProjectURL string // "/<project>/"
	ProjectAbs string // <fsRoot>/<project>

	Slot    string // "ssr", "mdl", or "rsk"
	SlotURL string // "/<project>/<slot>/"

	// Populated for VirtualView*Spec kinds: "table.yaml" or "form.yaml".
	SpecBase string

	// Populated for VirtualView*Row kinds.
	Party        string // party folder name (e.g. "0330C1")
	PartyArchive string // <fsRoot>/<project>/archive/<party>
	CanonicalAbs string // underlying file on disk
	CanonicalURL string // /<project>/archive/<party>/...
	SchemaAbs    string // SSR only — <party>/ssr.form.yaml (may not exist; falls back to embedded)
	RowFilename  string // MDL/RSK rollups only — e.g. "D-001.yaml"
}

// virtualViewRE matches /<project>/<slot>[/<rest>] where slot is one
// of the canonical virtual view names. Capture 1 = project, capture
// 2 = slot, capture 3 = rest (may be empty).
var virtualViewRE = regexp.MustCompile(`^/([^/]+)/(ssr|mdl|rsk)(?:/(.*))?$`)

// partyNameRE matches the SSR schema's `name` pattern. Same regex
// used at row-resolution time so URLs with invalid party tokens fail
// resolution cleanly instead of producing impossible canonical paths.
var partyNameRE = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9.-]*$`)

// ValidPartyName reports whether s is a valid party folder name —
// starts with [A-Za-z0-9], then any of [A-Za-z0-9.-]. Used by URL
// resolution AND by the SSR create handler to validate user input.
func ValidPartyName(s string) bool {
	return partyNameRE.MatchString(s)
}

// ResolveVirtualView inspects urlPath and returns a populated
// resolution iff the URL targets one of the project-level virtual
// views (ssr/, mdl/, rsk/). On a non-match, Resolved=false.
//
// The resolver does NOT check that the project / party / row file
// actually exist on disk — that's the caller's job (handlers use
// the canonical path; listings synthesize from real disk state).
//
// urlPath must be a server-relative URL with one leading slash.
// Trailing slashes are tolerated for root kinds.
func ResolveVirtualView(fsRoot, urlPath string) VirtualViewResolution {
	var out VirtualViewResolution
	if urlPath == "" || urlPath[0] != '/' {
		return out
	}
	trimmed := strings.TrimSuffix(urlPath, "/")
	m := virtualViewRE.FindStringSubmatch(trimmed)
	if m == nil {
		return out
	}
	project := m[1]
	slot := m[2]
	rest := m[3]

	if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
		return out
	}
	projectAbs := filepath.Join(fsRoot, filepath.FromSlash(project))
	if !strings.HasPrefix(projectAbs, fsRoot+string(filepath.Separator)) && projectAbs != fsRoot {
		return out
	}

	out.Project = project
	out.ProjectURL = "/" + project + "/"
	out.ProjectAbs = projectAbs
	out.Slot = slot
	out.SlotURL = "/" + project + "/" + slot + "/"

	if rest == "" {
		switch slot {
		case "ssr":
			out.Kind = VirtualViewSSRRoot
		case "mdl":
			out.Kind = VirtualViewMDLRoot
		case "rsk":
			out.Kind = VirtualViewRSKRoot
		}
		out.Resolved = true
		return out
	}

	if rest == "table.yaml" || rest == "form.yaml" {
		switch slot {
		case "ssr":
			out.Kind = VirtualViewSSRSpec
		case "mdl":
			out.Kind = VirtualViewMDLSpec
		case "rsk":
			out.Kind = VirtualViewRSKSpec
		}
		out.SpecBase = rest
		out.Resolved = true
		return out
	}

	// Row files — must be a single segment ending in .yaml.
	if strings.Contains(rest, "/") || !strings.HasSuffix(rest, ".yaml") {
		return out
	}
	name := strings.TrimSuffix(rest, ".yaml")

	if slot == "ssr" {
		if !ValidPartyName(name) {
			return out
		}
		out.Party = name
		out.PartyArchive = filepath.Join(projectAbs, "archive", name)
		out.CanonicalAbs = filepath.Join(out.PartyArchive, "ssr.yaml")
		out.CanonicalURL = "/" + project + "/archive/" + name + "/ssr.yaml"
		out.SchemaAbs = filepath.Join(out.PartyArchive, "ssr.form.yaml")
		out.Kind = VirtualViewSSRRow
		out.Resolved = true
		return out
	}

	// MDL/RSK rollup row — <party>__<file>.yaml.
	idx := strings.Index(name, "__")
	if idx <= 0 || idx >= len(name)-2 {
		return out
	}
	party := name[:idx]
	rowBase := name[idx+2:]
	if !ValidPartyName(party) || rowBase == "" || strings.Contains(rowBase, "__") {
		return out
	}
	out.Party = party
	out.PartyArchive = filepath.Join(projectAbs, "archive", party)
	out.RowFilename = rowBase + ".yaml"
	out.CanonicalAbs = filepath.Join(out.PartyArchive, slot, out.RowFilename)
	out.CanonicalURL = "/" + project + "/archive/" + party + "/" + slot + "/" + out.RowFilename
	switch slot {
	case "mdl":
		out.Kind = VirtualViewMDLRow
	case "rsk":
		out.Kind = VirtualViewRSKRow
	}
	out.Resolved = true
	return out
}

// IsSSRCreateURL reports whether urlPath is /<project>/ssr/form.html
// — the SSR "+ Add row" target. Returns the project name when matched.
func IsSSRCreateURL(urlPath string) (string, bool) {
	if urlPath == "" || urlPath[0] != '/' {
		return "", false
	}
	parts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
	if len(parts) != 3 || parts[1] != "ssr" || parts[2] != "form.html" {
		return "", false
	}
	project := parts[0]
	if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
		return "", false
	}
	return project, true
}

// IsRollupCreateURL reports whether urlPath is
// /<project>/(mdl|rsk)/form.html — the "+ Add row" target on a
// project-level MDL or RSK rollup view. Returns the project name +
// slot ("mdl" or "rsk") when matched. The rollup-create handler
// reads a `party` field from the body and routes the new row into
// <project>/archive/<party>/<slot>/.
func IsRollupCreateURL(urlPath string) (project, slot string, ok bool) {
	if urlPath == "" || urlPath[0] != '/' {
		return "", "", false
	}
	parts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
	if len(parts) != 3 || parts[2] != "form.html" {
		return "", "", false
	}
	if parts[1] != "mdl" && parts[1] != "rsk" {
		return "", "", false
	}
	project = parts[0]
	if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
		return "", "", false
	}
	return project, parts[1], true
}

// StripYAMLHTML returns urlPath with a trailing ".html" stripped iff
// the URL has the form-edit shape ".../<name>.yaml.html". Otherwise
// returns urlPath unchanged + false. The form recognizer calls this
// before passing the data URL into ResolveVirtualView.
func StripYAMLHTML(urlPath string) (string, bool) {
	if strings.HasSuffix(urlPath, ".yaml.html") {
		return strings.TrimSuffix(urlPath, ".html"), true
	}
	return urlPath, false
}

// ListSSRParties returns the party folder names that exist under
// <project>/archive/. Names are filtered through ValidPartyName so a
// hand-created folder with a weird name (e.g. "0330C1 (draft)") won't
// confuse the rest of the resolver. Returns nil + nil when archive/
// doesn't exist on disk.
func ListSSRParties(fsRoot, projectAbs string) ([]string, error) {
	archive := filepath.Join(projectAbs, "archive")
	entries, err := os.ReadDir(archive)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}
	out := make([]string, 0, len(entries))
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		name := e.Name()
		if !ValidPartyName(name) {
			continue
		}
		out = append(out, name)
	}
	sort.Strings(out)
	return out, nil
}

// VirtualRollupRow describes one synthetic row in a project-level
// MDL or RSK rollup.
type VirtualRollupRow struct {
	Party         string // source party folder
	Filename      string // e.g. "D-001.yaml"
	SyntheticName string // e.g. "0330C1__D-001.yaml" — used in URLs
	CanonicalAbs  string // underlying file on disk
}

// ListRollupRows walks <project>/archive/*/<slot>/ and returns one
// synthetic row per *.yaml file. slot must be "mdl" or "rsk".
// Returns rows sorted by (party, filename).
//
// Skipped:
//   - filenames containing "__" (would break the party__file split)
//   - "table.yaml" and "form.yaml" (operator spec/schema, not rows)
//   - any non-*.yaml file
//   - parties with invalid folder names (filtered by ListSSRParties)
func ListRollupRows(fsRoot, projectAbs, slot string) ([]VirtualRollupRow, error) {
	if slot != "mdl" && slot != "rsk" {
		return nil, errors.New("ListRollupRows: slot must be mdl or rsk")
	}
	parties, err := ListSSRParties(fsRoot, projectAbs)
	if err != nil {
		return nil, err
	}
	out := make([]VirtualRollupRow, 0, len(parties))
	for _, party := range parties {
		slotDir := filepath.Join(projectAbs, "archive", party, slot)
		entries, err := os.ReadDir(slotDir)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				continue
			}
			return nil, err
		}
		for _, e := range entries {
			if e.IsDir() {
				continue
			}
			name := e.Name()
			if !strings.HasSuffix(name, ".yaml") {
				continue
			}
			if name == "table.yaml" || name == "form.yaml" {
				continue
			}
			if strings.Contains(name, "__") {
				continue
			}
			out = append(out, VirtualRollupRow{
				Party:         party,
				Filename:      name,
				SyntheticName: party + "__" + name,
				CanonicalAbs:  filepath.Join(slotDir, name),
			})
		}
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].Party != out[j].Party {
			return out[i].Party < out[j].Party
		}
		return out[i].Filename < out[j].Filename
	})
	return out, nil
}
