package zddc

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

// Virtual `received/` window — the doc-controller's Plan Review composite
// endpoint scaffolds physical folders under archive/<party>/reviewing/ and
// archive/<party>/staging/, each carrying a .zddc whose `received_path:`
// points back at the canonical archive/<party>/received/<tracking>/. When
// a workflow folder is listed, the server injects a synthetic `received/`
// child that shows the canonical submittal's contents in context.
//
// Three behaviours rely on this:
//
//	GET <workflow>/received/                  → list the canonical received/<tracking>/
//	GET <workflow>/received/<file>            → serve canonical bytes (read passthrough)
//	PUT <workflow>/received/<file>            → rewrite to <workflow>/<base>+C<n><suffix>
//	                                             (the canonical record is WORM)
//
// Helpers below give the file API and listing handlers a single point of
// detection so the routing stays declarative.

// IsWorkflowFolder reports whether dirPath has a .zddc with a non-empty
// ReceivedPath — i.e. it's a Plan-Review-scaffolded reviewing/ or staging/
// folder.
func IsWorkflowFolder(dirPath string) bool {
	rp := WorkflowReceivedPath(dirPath)
	return rp != ""
}

// WorkflowReceivedPath returns the .zddc.received_path for dirPath, or
// empty if the file doesn't exist or doesn't declare one. The path is
// as-stored (typically relative to the master root).
func WorkflowReceivedPath(dirPath string) string {
	zf, err := ParseFile(filepath.Join(dirPath, ".zddc"))
	if err != nil {
		return ""
	}
	return zf.ReceivedPath
}

// VirtualReceivedResolution captures the result of mapping a request URL
// onto either a workflow folder's synthetic `received/` child or a
// canonical path under it. All fields are populated only when Resolved
// is true.
type VirtualReceivedResolution struct {
	Resolved      bool
	WorkflowAbs   string // absolute path of the workflow folder
	WorkflowURL   string // server-relative URL of the workflow folder, slash-terminated (e.g. "/Project/archive/Acme/reviewing/2026-05-30_X (TBD) - …/")
	ReceivedAbs   string // absolute path of the canonical received target (or canonical+suffix when the URL drills into a file)
	ReceivedURL   string // server-relative URL of the canonical received target
	SuffixURL     string // URL suffix after the `/received/` segment, slash-prefixed when non-empty (e.g. "" or "Acme-0042_A (RFI) - Foundation.pdf")
	IsRoot        bool   // true iff the URL targets `<workflow>/received/` itself (no suffix)
}

// virtualReceivedRE matches any URL that traverses a `received` segment
// not at the canonical archive/<party>/received/<tracking>/ position.
// The match is loose; the resolver verifies the parent .zddc carries a
// ReceivedPath before returning Resolved=true.
//
// Captures:
//   1: workflow URL prefix (including trailing slash before "received")
//   2: suffix after "received/" (may be empty)
var virtualReceivedRE = regexp.MustCompile(`^(/.+/)received(?:/(.*))?$`)

// ResolveVirtualReceived inspects urlPath and returns a populated
// resolution iff:
//
//   - the URL contains a `received/` segment whose parent on disk is a
//     workflow folder (.zddc.received_path is set), AND
//   - the URL is NOT the canonical archive/<party>/received/<tracking>/[...]
//     form (handlers there go through normal routing).
//
// The canonical form is detected by checking the .zddc.received_path of
// the parent — if the parent's path matches what received_path points at,
// that's the canonical record, not a synthetic mapping.
//
// On a non-match, Resolved=false and other fields are zero.
//
// urlPath is the server-relative URL with one leading slash. trailingSlash
// indicates whether the original URL ended with a slash (meaning a directory
// listing was requested vs a file).
func ResolveVirtualReceived(fsRoot, urlPath string) VirtualReceivedResolution {
	var out VirtualReceivedResolution
	if urlPath == "" || urlPath[0] != '/' {
		return out
	}
	trimmed := strings.TrimSuffix(urlPath, "/")
	m := virtualReceivedRE.FindStringSubmatch(trimmed)
	if m == nil {
		return out
	}
	workflowURL := m[1]
	suffix := m[2]

	// Translate workflow URL → workflow absolute path.
	workflowRel := strings.TrimPrefix(strings.TrimSuffix(workflowURL, "/"), "/")
	workflowAbs := filepath.Join(fsRoot, filepath.FromSlash(workflowRel))
	if !strings.HasPrefix(workflowAbs, fsRoot+string(filepath.Separator)) && workflowAbs != fsRoot {
		return out
	}

	// Workflow folder must carry a .zddc.received_path.
	rp := WorkflowReceivedPath(workflowAbs)
	if rp == "" {
		return out
	}

	// Guard: if workflowAbs itself happens to be the canonical received
	// folder for some weird cascade, don't loop on it. The canonical
	// record never has a .zddc declaring its own received_path, so this
	// can only happen with operator misconfiguration; bail out.
	receivedRel := filepath.ToSlash(filepath.Clean(rp))
	if filepath.ToSlash(strings.TrimPrefix(workflowAbs, fsRoot+string(filepath.Separator))) == receivedRel {
		return out
	}

	receivedAbs := filepath.Join(fsRoot, filepath.FromSlash(receivedRel))
	receivedURL := "/" + receivedRel + "/"

	if suffix != "" {
		// File or sub-path drill-in. Append to both abs and URL.
		receivedAbs = filepath.Join(receivedAbs, filepath.FromSlash(suffix))
		receivedURL = "/" + receivedRel + "/" + suffix
		if strings.HasSuffix(urlPath, "/") {
			receivedURL += "/"
		}
	}

	out.Resolved = true
	out.WorkflowAbs = workflowAbs
	out.WorkflowURL = workflowURL
	out.ReceivedAbs = receivedAbs
	out.ReceivedURL = receivedURL
	out.SuffixURL = suffix
	out.IsRoot = suffix == ""
	return out
}

// commentFilenameRE captures the canonical filename shape with an
// optional +Cn modifier already on the revision, so we can compute the
// next n for a target.
//
// Captures:
//
//	1: tracking, e.g. "Acme-0042"
//	2: revision base (without +C<n>), e.g. "A"
//	3: existing +C<n> number (may be empty if no modifier), e.g. "1"
//	4: rest of the filename, e.g. " (RFI) - Foundation.pdf"
var commentFilenameRE = regexp.MustCompile(`^([^_]+)_([^+\s()]+)(?:\+C(\d+))?(\s*\([^)]+\)\s*-\s*.+)$`)

// CommentResolvedName computes the next +Cn comment filename for the
// given target name inside workflowAbs. The target name is the file the
// user dropped onto (e.g. "Acme-0042_A (RFI) - Foundation.pdf"); the
// returned name has a `+C<n>` modifier on the revision token. n starts
// at 1 and increments past any existing comments for the same target.
//
// If targetName doesn't match the canonical ZDDC filename pattern, an
// error is returned — comment uploads are only meaningful against
// parseable submittals.
func CommentResolvedName(workflowAbs, targetName string) (string, error) {
	m := commentFilenameRE.FindStringSubmatch(targetName)
	if m == nil {
		return "", errors.New("target filename does not match the ZDDC pattern")
	}
	tracking := m[1]
	baseRev := m[2]
	rest := m[4]

	// Scan workflowAbs for siblings matching <tracking>_<baseRev>+C<n><rest>.
	entries, err := os.ReadDir(workflowAbs)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return "", err
	}
	maxN := 0
	prefix := tracking + "_" + baseRev + "+C"
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		name := e.Name()
		if !strings.HasPrefix(name, prefix) {
			continue
		}
		mm := commentFilenameRE.FindStringSubmatch(name)
		if mm == nil || mm[1] != tracking || mm[2] != baseRev || mm[3] == "" {
			continue
		}
		var n int
		for _, ch := range mm[3] {
			if ch < '0' || ch > '9' {
				n = 0
				break
			}
			n = n*10 + int(ch-'0')
		}
		if n > maxN {
			maxN = n
		}
	}
	return tracking + "_" + baseRev + "+C" + itoa(maxN+1) + rest, nil
}

// itoa is a tiny base-10 stringifier — small enough not to pull strconv.
func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var buf [10]byte
	i := len(buf)
	for n > 0 {
		i--
		buf[i] = byte('0' + n%10)
		n /= 10
	}
	return string(buf[i:])
}
