package patch

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
)

// symbolsByFeature maps each drop-in feature to the canonical top-level Go
// symbols it introduces. If any of these symbols is declared in an existing
// file under internal/cli/ — even under an unexpected filename — that's a
// resource-level collision: writing our drop-in would produce a `redeclared
// in this block` compile error.
//
// Only top-level func/type declarations are flagged; identifiers used
// inside other functions are not a collision.
var symbolsByFeature = map[string][]string{
	"profile":  {"newProfileCmd", "GetProfile", "ApplyProfileToFlags", "Profile"},
	"deliver":  {"DeliverSink", "ParseDeliverSink", "Deliver"},
	"feedback": {"newFeedbackCmd", "FeedbackEntry"},
}

// topLevelDeclRe matches `func Name(` and `type Name ` at the start of a
// line — where top-level declarations live after gofmt. Per-function
// references (e.g. a call to Deliver inside some other handler) are not
// flagged because they don't redeclare the symbol.
var topLevelDeclRe = regexp.MustCompile(`(?m)^(?:func|type)\s+(\w+)\b`)

// detectCollisions scans the CLI's internal/cli directory for conflicts with
// the symbols PR #218 introduces. Returns all collisions; the caller decides
// which are fatal.
//
// A resource-level collision (another file in internal/cli declaring one of
// our drop-in's top-level symbols) is fatal — dropping our file in would
// produce `redeclared in this block`. Excludes the three drop-in filenames
// themselves since they may already be present from a prior patch run (in
// which case detection is idempotency, not collision).
func detectCollisions(dir string, rootSrc []byte) ([]Collision, error) {
	cliDir := filepath.Join(dir, "internal", "cli")

	// Pass 1: the drop-in filenames themselves. If they exist and don't look
	// like our template output, that's a collision (same shape as before).
	dropinFiles := map[string]string{
		"profile.go":  "profile",
		"deliver.go":  "deliver",
		"feedback.go": "feedback",
	}
	var collisions []Collision
	for name, feature := range dropinFiles {
		path := filepath.Join(cliDir, name)
		data, err := os.ReadFile(path)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, fmt.Errorf("reading %s: %w", path, err)
		}
		if bytes.Contains(data, []byte("Generated by CLI Printing Press")) &&
			bytes.Contains(data, []byte("DO NOT EDIT")) &&
			looksLikePR218Dropin(name, data) {
			continue
		}
		collisions = append(collisions, Collision{
			Kind:    "resource",
			Symbol:  feature,
			File:    path,
			Message: fmt.Sprintf("%s already exists and is not a PR #218 drop-in — refusing to overwrite", path),
		})
	}

	// Pass 2: any OTHER file under internal/cli that declares one of our
	// drop-in's top-level symbols. Catches cases like steam-web, which has a
	// pre-existing cmd_profile.go that defines newProfileCmd.
	entries, err := os.ReadDir(cliDir)
	if err != nil {
		if os.IsNotExist(err) {
			return collisions, nil
		}
		return nil, fmt.Errorf("reading %s: %w", cliDir, err)
	}
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		name := entry.Name()
		if _, isDropin := dropinFiles[name]; isDropin {
			continue // handled in pass 1
		}
		if filepath.Ext(name) != ".go" || hasTestSuffix(name) {
			continue
		}
		path := filepath.Join(cliDir, name)
		data, err := os.ReadFile(path)
		if err != nil {
			return nil, fmt.Errorf("reading %s: %w", path, err)
		}
		for _, match := range topLevelDeclRe.FindAllStringSubmatch(string(data), -1) {
			for feature, syms := range symbolsByFeature {
				if featureAlreadyColliding(collisions, feature) {
					continue
				}
				for _, sym := range syms {
					if match[1] == sym {
						collisions = append(collisions, Collision{
							Kind:    "resource",
							Symbol:  feature,
							File:    path,
							Message: fmt.Sprintf("%s declares %s, which would conflict with PR #218's %s drop-in", path, sym, feature),
						})
					}
				}
			}
		}
	}
	return collisions, nil
}

func hasTestSuffix(name string) bool {
	return len(name) > len("_test.go") && name[len(name)-len("_test.go"):] == "_test.go"
}

func featureAlreadyColliding(collisions []Collision, feature string) bool {
	for _, c := range collisions {
		if c.Symbol == feature {
			return true
		}
	}
	return false
}

// looksLikePR218Dropin returns true when the file content matches the
// signature of the template output for profile/deliver/feedback. Cheap
// heuristic: check for the canonical function name each template defines.
func looksLikePR218Dropin(filename string, data []byte) bool {
	switch filename {
	case "profile.go":
		return bytes.Contains(data, []byte("func newProfileCmd(")) &&
			bytes.Contains(data, []byte("func GetProfile("))
	case "deliver.go":
		return bytes.Contains(data, []byte("type DeliverSink")) &&
			bytes.Contains(data, []byte("func ParseDeliverSink("))
	case "feedback.go":
		return bytes.Contains(data, []byte("func newFeedbackCmd(")) &&
			bytes.Contains(data, []byte("type FeedbackEntry"))
	}
	return false
}
