// Package gitoevents consumes the Gito-provided proto-socket branch event
// contract (gito.forgejo-branch-events.v1) inside NomadCode core.
//
// 책임 경계: NomadCode core는 `branch.updated`를 wakeup signal로만 소비한다.
// `changed_files`는 roadmap sync 관련성 판단을 위한 힌트이며, target branch
// revision의 실제 검증/스캔은 후속 bridge(revision-scan)의 책임이다.
package gitoevents

import (
	"fmt"
	"strings"

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

const (
	// EventChannel is the proto-socket channel used for Gito branch events.
	EventChannel = "event"
	// SubscribeAction is the action used to request a subscription.
	SubscribeAction = "event.subscribe"
	// BranchUpdatedAction is the broadcast event action emitted by Gito.
	BranchUpdatedAction = "branch.updated"

	// DefaultBranch is used when no branch is configured.
	DefaultBranch = "develop"
)

// SubscribeRequest is the payload sent to Gito to subscribe to branch events.
type SubscribeRequest struct {
	Events []string `json:"events"`
	RepoID string   `json:"repo_id"`
	Branch string   `json:"branch"`
}

// ChangedFile is a single file changed by a push, used only as a wakeup hint.
type ChangedFile struct {
	Path       string `json:"path"`
	ChangeType string `json:"change_type"`
}

// BranchUpdatedEvent is the decoded `branch.updated` broadcast payload.
type BranchUpdatedEvent struct {
	ID           string        `json:"id"`
	Type         string        `json:"type"`
	Provider     string        `json:"provider"`
	DeliveryID   string        `json:"delivery_id"`
	RepoID       string        `json:"repo_id"`
	Branch       string        `json:"branch"`
	Before       string        `json:"before"`
	After        string        `json:"after"`
	ChangedFiles []ChangedFile `json:"changed_files"`
	ObservedAt   string        `json:"observed_at"`
	CreatedAt    string        `json:"created_at"`
}

// BuildSubscribeEnvelope builds the `event.subscribe` request envelope for the
// given repo and branch. An empty branch falls back to DefaultBranch.
func BuildSubscribeEnvelope(id, repoID, branch string) protosocket.Envelope {
	if branch == "" {
		branch = DefaultBranch
	}
	return protosocket.Envelope{
		ProtocolVersion: protosocket.ProtocolVersion,
		ID:              id,
		Type:            "request",
		Channel:         EventChannel,
		Action:          SubscribeAction,
		Payload: map[string]any{
			"events":  []any{BranchUpdatedAction},
			"repo_id": repoID,
			"branch":  branch,
		},
	}
}

// DecodeBranchUpdatedPayload decodes a `branch.updated` base payload into a
// BranchUpdatedEvent. It validates required fields (repo_id, branch) and the
// type field, and decodes optional fields without requiring them. This function
// has no envelope knowledge so it can be shared between proto-socket envelope
// and raw HTTP JSON body consumers.
//
// Constraints:
//
//   - type must be "branch.updated" if present
//   - repo_id and branch are required
//   - changed_files must be a list of objects if present
func DecodeBranchUpdatedPayload(payload map[string]any) (BranchUpdatedEvent, error) {
	if payload == nil {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: nil payload")
	}

	ev := BranchUpdatedEvent{}

	ev.ID = stringField(payload, "id")
	ev.Type = stringField(payload, "type")
	ev.Provider = stringField(payload, "provider")
	ev.DeliveryID = stringField(payload, "delivery_id")
	ev.RepoID = stringField(payload, "repo_id")
	ev.Branch = stringField(payload, "branch")
	ev.Before = stringField(payload, "before")
	ev.After = stringField(payload, "after")
	ev.ObservedAt = stringField(payload, "observed_at")
	ev.CreatedAt = stringField(payload, "created_at")

	// Validate required fields
	if ev.RepoID == "" {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload missing required field repo_id")
	}
	if ev.Branch == "" {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload missing required field branch")
	}

	// Validate type field if present
	if ev.Type != "" && ev.Type != BranchUpdatedAction {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: payload type %q, want %q", ev.Type, BranchUpdatedAction)
	}

	// Decode changed_files (optional but must be valid if present)
	files, err := decodeChangedFiles(payload["changed_files"])
	if err != nil {
		return BranchUpdatedEvent{}, err
	}
	ev.ChangedFiles = files

	return ev, nil
}

// DecodeBranchUpdatedEnvelope validates the envelope channel/action and decodes
// its payload into a BranchUpdatedEvent. It rejects wrong channel/action and
// malformed payloads with a descriptive error instead of returning a partial
// event.
func DecodeBranchUpdatedEnvelope(env protosocket.Envelope) (BranchUpdatedEvent, error) {
	if env.Channel != EventChannel {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected channel %q, want %q", env.Channel, EventChannel)
	}
	if env.Action != BranchUpdatedAction {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: unexpected action %q, want %q", env.Action, BranchUpdatedAction)
	}
	if env.Payload == nil {
		return BranchUpdatedEvent{}, fmt.Errorf("gitoevents: missing payload")
	}

	return DecodeBranchUpdatedPayload(env.Payload)
}

// IsTarget reports whether the event matches the watched repo and branch.
func (e BranchUpdatedEvent) IsTarget(repoID, branch string) bool {
	if branch == "" {
		branch = DefaultBranch
	}
	return e.RepoID == repoID && e.Branch == branch
}

// MilestoneChangedFiles returns the changed files that look like roadmap
// Milestone documents (`agent-roadmap/phase/<phase>/milestones/<slug>.md`).
// This is a wakeup-relevance hint only; revision verification is downstream.
func (e BranchUpdatedEvent) MilestoneChangedFiles() []ChangedFile {
	var out []ChangedFile
	for _, f := range e.ChangedFiles {
		if isMilestonePath(f.Path) {
			out = append(out, f)
		}
	}
	return out
}

func isMilestonePath(path string) bool {
	p := strings.TrimPrefix(path, "./")
	if !strings.HasPrefix(p, "agent-roadmap/phase/") {
		return false
	}
	if !strings.HasSuffix(p, ".md") {
		return false
	}
	return strings.Contains(p, "/milestones/")
}

func decodeChangedFiles(raw any) ([]ChangedFile, error) {
	if raw == nil {
		return nil, nil
	}
	list, ok := raw.([]any)
	if !ok {
		return nil, fmt.Errorf("gitoevents: changed_files is not a list")
	}
	out := make([]ChangedFile, 0, len(list))
	for i, item := range list {
		m, ok := item.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("gitoevents: changed_files[%d] is not an object", i)
		}
		out = append(out, ChangedFile{
			Path:       stringField(m, "path"),
			ChangeType: stringField(m, "change_type"),
		})
	}
	return out, nil
}

func stringField(m map[string]any, key string) string {
	if v, ok := m[key].(string); ok {
		return v
	}
	return ""
}
