package roadmapsync

import (
	"bufio"
	"errors"
	"strings"

	"github.com/nomadcode/nomadcode-core/internal/workitem"
)

// ErrNoProviderIdentity is returned when a Milestone document carries no
// provider identity block. A Milestone without provider + work item id cannot
// be matched to a Plane ticket, so this is an explicit miss rather than a
// silently empty success.
var ErrNoProviderIdentity = errors.New("milestone has no provider identity")

// identityFieldKeys maps the bullet/field labels accepted inside a provider
// identity block to the Identity field they populate. Keys are matched
// case-insensitively after trimming, with both spaced and snake_case spellings
// accepted so the authoring agent's Markdown does not have to match one exact
// form.
var identityFieldKeys = map[string]string{
	"provider":            "provider",
	"tenant":              "tenant",
	"project":             "project",
	"work item id":        "work_item_id",
	"work_item_id":        "work_item_id",
	"parent work item id": "parent_work_item_id",
	"parent_work_item_id": "parent_work_item_id",
	"roadmap item id":     "roadmap_item_id",
	"roadmap_item_id":     "roadmap_item_id",
	"milestone id":        "roadmap_item_id",
}

// identityHeading is the section/bullet label that opens a provider identity
// block. Matched case-insensitively.
const identityHeading = "provider identity"

// ParseMilestoneIdentity reads a strict provider identity block out of a
// Milestone Markdown document and returns a normalized Identity. milestonePath
// is the repository-relative path of the Milestone file; it becomes the
// identity's RoadmapMilestonePath since the path is not duplicated inside the
// block. ad hoc string scanning is confined to this function; the external
// result is a typed Identity only.
//
// The block is recognized either as a heading ("## Provider identity") followed
// by `- field: value` bullets, or as a single "Provider identity" bullet whose
// nested bullets carry the fields. Parsing stops at the next heading or the end
// of the indented bullet group.
func ParseMilestoneIdentity(milestonePath, markdown string) (Identity, error) {
	path := strings.TrimSpace(milestonePath)
	if path == "" {
		return Identity{}, ErrInvalidIdentity
	}

	fields, found := scanIdentityFields(markdown)
	if !found || len(fields) == 0 {
		return Identity{}, ErrNoProviderIdentity
	}

	identity := Identity{
		Shape:                ShapeMilestone,
		RoadmapMilestonePath: path,
		RoadmapItemID:        fields["roadmap_item_id"],
		Provider:             workitem.ProviderID(fields["provider"]),
		Tenant:               fields["tenant"],
		Project:              fields["project"],
		WorkItemID:           fields["work_item_id"],
		ParentWorkItemID:     fields["parent_work_item_id"],
	}
	if identity.ParentWorkItemID != "" {
		identity.Shape = ShapeTask
	}

	normalized, err := identity.Normalize()
	if err != nil {
		// A present-but-incomplete identity block is a provider identity miss,
		// not a generic invalid-identity, so callers can distinguish "no block"
		// from "block missing required fields" via the same not-ready signal.
		return Identity{}, ErrNoProviderIdentity
	}
	return normalized, nil
}

// blockKind distinguishes how the open provider identity block was introduced,
// because the two forms have different boundaries.
type blockKind int

const (
	blockNone    blockKind = iota
	blockHeading           // "## Provider identity" heading; fields run until the next heading.
	blockBullet            // "- Provider identity:" bullet; fields are only deeper-indented child bullets.
)

// scanIdentityFields walks the document looking for the provider identity block
// and collects its field/value pairs keyed by the normalized Identity field
// name. The second return reports whether the block heading/bullet was found at
// all, so a present-but-empty block is distinguishable from an absent one.
//
// Heading blocks ("## Provider identity") accept field lines until the next
// heading. Bullet blocks ("- Provider identity:") only accept child bullets
// indented deeper than the parent bullet; a blank line, an outdent, a same- or
// shallower-indent sibling, or a heading closes the bullet block so a later
// unrelated `provider:`/`work item id:` bullet is never absorbed.
func scanIdentityFields(markdown string) (map[string]string, bool) {
	fields := map[string]string{}
	scanner := bufio.NewScanner(strings.NewReader(markdown))

	kind := blockNone
	found := false
	parentIndent := 0
	for scanner.Scan() {
		raw := scanner.Text()
		line := strings.TrimSpace(raw)

		if line == "" {
			// A blank line ends a bullet block; heading blocks tolerate blanks.
			if kind == blockBullet {
				kind = blockNone
			}
			continue
		}

		if isMarkdownHeading(line) {
			if headingMatchesIdentity(line) {
				kind = blockHeading
				found = true
				continue
			}
			// Any other heading closes an open block.
			if kind != blockNone {
				kind = blockNone
			}
			continue
		}

		indent := leadingIndent(raw)
		key, value, ok := parseFieldLine(line)
		if !ok {
			// A non field-shaped line breaks an open bullet block.
			if kind == blockBullet {
				kind = blockNone
			}
			continue
		}

		// A standalone "Provider identity" bullet opens a bullet block; only its
		// deeper-indented child bullets count as fields.
		if kind == blockNone && strings.EqualFold(key, identityHeading) {
			kind = blockBullet
			found = true
			parentIndent = indent
			continue
		}

		switch kind {
		case blockBullet:
			if indent <= parentIndent {
				// Sibling or outdent: the bullet block is over. The current line
				// is not a child field; do not absorb it.
				kind = blockNone
				continue
			}
		case blockHeading:
			// Heading blocks accept any field line until the next heading.
		default:
			continue
		}

		if field, known := identityFieldKeys[strings.ToLower(key)]; known && value != "" {
			fields[field] = value
		}
	}
	return fields, found
}

// leadingIndent counts the leading whitespace columns of a raw line, treating a
// tab as one column. Only relative depth between the parent bullet and its
// children matters, so an exact column model is unnecessary.
func leadingIndent(raw string) int {
	return len(raw) - len(strings.TrimLeft(raw, " \t"))
}

func isMarkdownHeading(line string) bool {
	return strings.HasPrefix(line, "#")
}

func headingMatchesIdentity(line string) bool {
	text := strings.TrimSpace(strings.TrimLeft(line, "#"))
	return strings.EqualFold(text, identityHeading)
}

// parseFieldLine extracts a `key: value` pair from a bullet or plain field
// line, stripping a leading list marker. It reports ok=false for lines that are
// not key/value shaped.
func parseFieldLine(line string) (key, value string, ok bool) {
	trimmed := strings.TrimSpace(strings.TrimLeft(line, "-*"))
	idx := strings.Index(trimmed, ":")
	if idx < 0 {
		return "", "", false
	}
	key = strings.TrimSpace(trimmed[:idx])
	value = strings.TrimSpace(trimmed[idx+1:])
	value = strings.Trim(value, "`")
	value = strings.TrimSpace(value)
	if key == "" {
		return "", "", false
	}
	return key, value, true
}
