package main

import (
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/formula"
	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/types"
	"github.com/steveyegge/beads/internal/ui"
	"github.com/steveyegge/beads/internal/utils"
)

var molBondCmd = &cobra.Command{
	Use:     "bond <A> <B>",
	Aliases: []string{"fart"}, // Easter egg: molecules can produce gas
	Short:   "Bond two protos or molecules together",
	Long: `Bond two protos or molecules to create a compound.

The bond command is polymorphic - it handles different operand types:

  formula + formula → cook both, compound proto
  formula + proto   → cook formula, compound proto
  formula + mol     → cook formula, spawn and attach
  proto + proto     → compound proto (reusable template)
  proto + mol       → spawn proto, attach to molecule
  mol + proto       → spawn proto, attach to molecule
  mol + mol         → join into compound molecule

Formula names (e.g., mol-polecat-arm) are cooked inline as ephemeral protos.
This avoids needing pre-cooked proto beads in the database.

Bond types:
  sequential (default) - B runs after A completes
  parallel            - B runs alongside A
  conditional         - B runs only if A fails

Phase control:
  By default, spawned protos follow the target's phase:
  - Attaching to mol (Ephemeral=false) → spawns as persistent (Ephemeral=false)
  - Attaching to ephemeral issue (Ephemeral=true) → spawns as ephemeral (Ephemeral=true)

  Override with:
  --pour  Force spawn as liquid (persistent, Ephemeral=false)
  --ephemeral  Force spawn as vapor (ephemeral, Ephemeral=true, excluded from JSONL export)

Dynamic bonding (Christmas Ornament pattern):
  Use --ref to specify a custom child reference with variable substitution.
  This creates IDs like "parent.child-ref" instead of random hashes.

  Example:
    bd mol bond mol-polecat-arm bd-patrol --ref arm-{{polecat_name}} --var polecat_name=ace
    # Creates: bd-patrol.arm-ace (and children like bd-patrol.arm-ace.capture)

Use cases:
  - Found important bug during patrol? Use --pour to persist it
  - Need ephemeral diagnostic on persistent feature? Use --ephemeral
  - Spawning per-worker arms on a patrol? Use --ref for readable IDs

Examples:
  bd mol bond mol-feature mol-deploy                    # Compound proto
  bd mol bond mol-feature mol-deploy --type parallel    # Run in parallel
  bd mol bond mol-feature bd-abc123                     # Attach proto to molecule
  bd mol bond bd-abc123 bd-def456                       # Join two molecules
  bd mol bond mol-critical-bug wisp-patrol --pour       # Persist found bug
  bd mol bond mol-temp-check bd-feature --ephemeral          # Ephemeral diagnostic
  bd mol bond mol-arm bd-patrol --ref arm-{{name}} --var name=ace  # Dynamic child ID`,
	Args: cobra.ExactArgs(2),
	Run:  runMolBond,
}

// BondResult holds the result of a bond operation
type BondResult struct {
	ResultID   string            `json:"result_id"`
	ResultType string            `json:"result_type"` // "compound_proto" or "compound_molecule"
	BondType   string            `json:"bond_type"`
	Spawned    int               `json:"spawned,omitempty"`    // Number of issues spawned (if proto was involved)
	IDMapping  map[string]string `json:"id_mapping,omitempty"` // Old ID -> new ID for spawned issues
}

// runMolBond implements the polymorphic bond command
func runMolBond(cmd *cobra.Command, args []string) {
	CheckReadonly("mol bond")

	ctx := rootCtx

	// mol bond requires direct store access
	if store == nil {
		if daemonClient != nil {
			fmt.Fprintf(os.Stderr, "Error: mol bond requires direct database access\n")
			fmt.Fprintf(os.Stderr, "Hint: use --no-daemon flag: bd --no-daemon mol bond %s %s ...\n", args[0], args[1])
		} else {
			fmt.Fprintf(os.Stderr, "Error: no database connection\n")
		}
		os.Exit(1)
	}

	bondType, _ := cmd.Flags().GetString("type")
	customTitle, _ := cmd.Flags().GetString("as")
	dryRun, _ := cmd.Flags().GetBool("dry-run")
	varFlags, _ := cmd.Flags().GetStringSlice("var")
	ephemeral, _ := cmd.Flags().GetBool("ephemeral")
	pour, _ := cmd.Flags().GetBool("pour")
	childRef, _ := cmd.Flags().GetString("ref")

	// Validate phase flags are not both set
	if ephemeral && pour {
		fmt.Fprintf(os.Stderr, "Error: cannot use both --ephemeral and --pour\n")
		os.Exit(1)
	}

	// All issues go in the main store; ephemeral vs pour determines the Wisp flag
	// --ephemeral: create with Ephemeral=true (ephemeral, excluded from JSONL export)
	// --pour: create with Ephemeral=false (persistent, exported to JSONL)
	// Default: follow target's phase (ephemeral if target is ephemeral, otherwise persistent)

	// Validate bond type
	if bondType != types.BondTypeSequential && bondType != types.BondTypeParallel && bondType != types.BondTypeConditional {
		fmt.Fprintf(os.Stderr, "Error: invalid bond type '%s', must be: sequential, parallel, or conditional\n", bondType)
		os.Exit(1)
	}

	// Parse variables
	vars := make(map[string]string)
	for _, v := range varFlags {
		parts := strings.SplitN(v, "=", 2)
		if len(parts) != 2 {
			fmt.Fprintf(os.Stderr, "Error: invalid variable format '%s', expected 'key=value'\n", v)
			os.Exit(1)
		}
		vars[parts[0]] = parts[1]
	}

	// For dry-run, just check if operands can be resolved (don't cook)
	if dryRun {
		issueA, formulaA, err := resolveOrDescribe(ctx, store, args[0])
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}
		issueB, formulaB, err := resolveOrDescribe(ctx, store, args[1])
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}

		idA := args[0]
		idB := args[1]
		aIsProto := false
		bIsProto := false

		if issueA != nil {
			idA = issueA.ID
			aIsProto = isProto(issueA)
		}
		if issueB != nil {
			idB = issueB.ID
			bIsProto = isProto(issueB)
		}

		// Formulas are treated as protos for dry-run display
		if formulaA != "" {
			aIsProto = true
		}
		if formulaB != "" {
			bIsProto = true
		}

		fmt.Printf("\nDry run: bond %s + %s\n", idA, idB)
		if formulaA != "" {
			fmt.Printf("  A: %s (formula → will cook as proto)\n", formulaA)
		} else if issueA != nil {
			fmt.Printf("  A: %s (%s)\n", issueA.Title, operandType(aIsProto))
		}
		if formulaB != "" {
			fmt.Printf("  B: %s (formula → will cook as proto)\n", formulaB)
		} else if issueB != nil {
			fmt.Printf("  B: %s (%s)\n", issueB.Title, operandType(bIsProto))
		}
		fmt.Printf("  Bond type: %s\n", bondType)
		if ephemeral {
			fmt.Printf("  Phase override: vapor (--ephemeral)\n")
		} else if pour {
			fmt.Printf("  Phase override: liquid (--pour)\n")
		}
		if childRef != "" {
			resolvedRef := substituteVariables(childRef, vars)
			fmt.Printf("  Child ref: %s (resolved: %s)\n", childRef, resolvedRef)
		}
		if aIsProto && bIsProto {
			fmt.Printf("  Result: compound proto\n")
			if customTitle != "" {
				fmt.Printf("  Custom title: %s\n", customTitle)
			}
		} else if aIsProto || bIsProto {
			fmt.Printf("  Result: spawn proto, attach to molecule\n")
		} else {
			fmt.Printf("  Result: compound molecule\n")
		}
		if formulaA != "" || formulaB != "" {
			fmt.Printf("\n  Note: Cooked formulas are ephemeral and deleted after bonding.\n")
		}
		return
	}

	// Resolve both operands - can be issue IDs or formula names
	// Formula names are cooked inline to in-memory subgraphs
	subgraphA, cookedA, err := resolveOrCookToSubgraph(ctx, store, args[0])
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
	subgraphB, cookedB, err := resolveOrCookToSubgraph(ctx, store, args[1])
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}

	// No cleanup needed - in-memory subgraphs don't pollute the DB
	issueA := subgraphA.Root
	issueB := subgraphB.Root
	idA := issueA.ID
	idB := issueB.ID

	// Determine operand types
	aIsProto := issueA.IsTemplate || cookedA
	bIsProto := issueB.IsTemplate || cookedB

	// Dispatch based on operand types
	// All operations use the main store; wisp flag determines ephemeral vs persistent
	var result *BondResult
	switch {
	case aIsProto && bIsProto:
		// Compound protos are templates - always persistent
		// Note: Proto+proto bonding from formulas is a DB operation, not in-memory
		result, err = bondProtoProto(ctx, store, issueA, issueB, bondType, customTitle, actor)
	case aIsProto && !bIsProto:
		// Pass subgraph directly if cooked from formula
		if cookedA {
			result, err = bondProtoMolWithSubgraph(ctx, store, subgraphA, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour)
		} else {
			result, err = bondProtoMol(ctx, store, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour)
		}
	case !aIsProto && bIsProto:
		// Pass subgraph directly if cooked from formula
		if cookedB {
			result, err = bondProtoMolWithSubgraph(ctx, store, subgraphB, issueB, issueA, bondType, vars, childRef, actor, ephemeral, pour)
		} else {
			result, err = bondMolProto(ctx, store, issueA, issueB, bondType, vars, childRef, actor, ephemeral, pour)
		}
	default:
		result, err = bondMolMol(ctx, store, issueA, issueB, bondType, actor)
	}

	if err != nil {
		fmt.Fprintf(os.Stderr, "Error bonding: %v\n", err)
		os.Exit(1)
	}

	// Schedule auto-flush - wisps are in main DB now, but JSONL export skips them
	markDirtyAndScheduleFlush()

	if jsonOutput {
		outputJSON(result)
		return
	}

	fmt.Printf("%s Bonded: %s + %s\n", ui.RenderPass("✓"), idA, idB)
	fmt.Printf("  Result: %s (%s)\n", result.ResultID, result.ResultType)
	if result.Spawned > 0 {
		fmt.Printf("  Spawned: %d issues\n", result.Spawned)
	}
	if ephemeral {
		fmt.Printf("  Phase: vapor (ephemeral, Ephemeral=true)\n")
	} else if pour {
		fmt.Printf("  Phase: liquid (persistent, Ephemeral=false)\n")
	}
}

// isProto checks if an issue is a proto (has the template label)
func isProto(issue *types.Issue) bool {
	for _, label := range issue.Labels {
		if label == MoleculeLabel {
			return true
		}
	}
	return false
}

// operandType returns a human-readable type string
func operandType(isProtoIssue bool) string {
	if isProtoIssue {
		return "proto"
	}
	return "molecule"
}

// bondProtoProto bonds two protos to create a compound proto
func bondProtoProto(ctx context.Context, s storage.Storage, protoA, protoB *types.Issue, bondType, customTitle, actorName string) (*BondResult, error) {
	// Create compound proto: a new root that references both protos as children
	// The compound root will be a new issue that ties them together
	compoundTitle := fmt.Sprintf("Compound: %s + %s", protoA.Title, protoB.Title)
	if customTitle != "" {
		compoundTitle = customTitle
	}

	var compoundID string
	err := s.RunInTransaction(ctx, func(tx storage.Transaction) error {
		// Create compound root issue
		compound := &types.Issue{
			Title:       compoundTitle,
			Description: fmt.Sprintf("Compound proto bonding %s and %s", protoA.ID, protoB.ID),
			Status:      types.StatusOpen,
			Priority:    minPriority(protoA.Priority, protoB.Priority),
			IssueType:   types.TypeEpic,
			BondedFrom: []types.BondRef{
				{ProtoID: protoA.ID, BondType: bondType, BondPoint: ""},
				{ProtoID: protoB.ID, BondType: bondType, BondPoint: ""},
			},
		}
		if err := tx.CreateIssue(ctx, compound, actorName); err != nil {
			return fmt.Errorf("creating compound: %w", err)
		}
		compoundID = compound.ID

		// Add template label (labels are stored separately, not in issue table)
		if err := tx.AddLabel(ctx, compoundID, MoleculeLabel, actorName); err != nil {
			return fmt.Errorf("adding template label: %w", err)
		}

		// Add parent-child dependencies from compound to both proto roots
		depA := &types.Dependency{
			IssueID:     protoA.ID,
			DependsOnID: compoundID,
			Type:        types.DepParentChild,
		}
		if err := tx.AddDependency(ctx, depA, actorName); err != nil {
			return fmt.Errorf("linking proto A: %w", err)
		}

		depB := &types.Dependency{
			IssueID:     protoB.ID,
			DependsOnID: compoundID,
			Type:        types.DepParentChild,
		}
		if err := tx.AddDependency(ctx, depB, actorName); err != nil {
			return fmt.Errorf("linking proto B: %w", err)
		}

		// For sequential/conditional bonding, add blocking dependency: B blocks on A
		// Sequential: B runs after A completes (any outcome)
		// Conditional: B runs only if A fails
		if bondType == types.BondTypeSequential || bondType == types.BondTypeConditional {
			depType := types.DepBlocks
			if bondType == types.BondTypeConditional {
				depType = types.DepConditionalBlocks
			}
			seqDep := &types.Dependency{
				IssueID:     protoB.ID,
				DependsOnID: protoA.ID,
				Type:        depType,
			}
			if err := tx.AddDependency(ctx, seqDep, actorName); err != nil {
				return fmt.Errorf("adding sequence dep: %w", err)
			}
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return &BondResult{
		ResultID:   compoundID,
		ResultType: "compound_proto",
		BondType:   bondType,
		Spawned:    0,
	}, nil
}

// bondProtoMol bonds a proto to an existing molecule by spawning the proto.
// If childRef is provided, generates custom IDs like "parent.childref" (dynamic bonding).
// protoSubgraph can be nil if proto is from DB (will be loaded), or pre-loaded for formulas.
func bondProtoMol(ctx context.Context, s storage.Storage, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) {
	return bondProtoMolWithSubgraph(ctx, s, nil, proto, mol, bondType, vars, childRef, actorName, ephemeralFlag, pourFlag)
}

// bondProtoMolWithSubgraph is the internal implementation that accepts a pre-loaded subgraph.
func bondProtoMolWithSubgraph(ctx context.Context, s storage.Storage, protoSubgraph *TemplateSubgraph, proto, mol *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) {
	// Use provided subgraph or load from DB
	subgraph := protoSubgraph
	if subgraph == nil {
		var err error
		subgraph, err = loadTemplateSubgraph(ctx, s, proto.ID)
		if err != nil {
			return nil, fmt.Errorf("loading proto: %w", err)
		}
	}

	// Check for missing variables
	requiredVars := extractAllVariables(subgraph)
	var missingVars []string
	for _, v := range requiredVars {
		if _, ok := vars[v]; !ok {
			missingVars = append(missingVars, v)
		}
	}
	if len(missingVars) > 0 {
		return nil, fmt.Errorf("missing required variables: %s (use --var)", strings.Join(missingVars, ", "))
	}

	// Determine ephemeral flag based on explicit flags or target's phase
	// --ephemeral: force ephemeral=true, --pour: force ephemeral=false, neither: follow target
	makeEphemeral := mol.Ephemeral // Default: follow target's phase
	if ephemeralFlag {
		makeEphemeral = true
	} else if pourFlag {
		makeEphemeral = false
	}

	// Build CloneOptions for spawning
	opts := CloneOptions{
		Vars:  vars,
		Actor: actorName,
		Ephemeral:  makeEphemeral,
	}

	// Dynamic bonding: use custom IDs if childRef is provided
	if childRef != "" {
		opts.ParentID = mol.ID
		opts.ChildRef = childRef
	}

	// Spawn the proto with options
	spawnResult, err := spawnMoleculeWithOptions(ctx, s, subgraph, opts)
	if err != nil {
		return nil, fmt.Errorf("spawning proto: %w", err)
	}

	// Attach spawned molecule to existing molecule
	err = s.RunInTransaction(ctx, func(tx storage.Transaction) error {
		// Add dependency from spawned root to molecule
		// Sequential: use blocks (B runs after A completes)
		// Conditional: use conditional-blocks (B runs only if A fails)
		// Parallel: use parent-child (organizational, no blocking)
		// Note: Schema only allows one dependency per (issue_id, depends_on_id) pair
		var depType types.DependencyType
		switch bondType {
		case types.BondTypeSequential:
			depType = types.DepBlocks
		case types.BondTypeConditional:
			depType = types.DepConditionalBlocks
		default:
			depType = types.DepParentChild
		}
		dep := &types.Dependency{
			IssueID:     spawnResult.NewEpicID,
			DependsOnID: mol.ID,
			Type:        depType,
		}
		return tx.AddDependency(ctx, dep, actorName)
		// Note: bonded_from field tracking is not yet supported by storage layer.
		// The dependency relationship captures the bonding semantics.
	})

	if err != nil {
		return nil, fmt.Errorf("attaching to molecule: %w", err)
	}

	return &BondResult{
		ResultID:   mol.ID,
		ResultType: "compound_molecule",
		BondType:   bondType,
		Spawned:    spawnResult.Created,
		IDMapping:  spawnResult.IDMapping,
	}, nil
}

// bondMolProto bonds a molecule to a proto (symmetric with bondProtoMol)
func bondMolProto(ctx context.Context, s storage.Storage, mol, proto *types.Issue, bondType string, vars map[string]string, childRef string, actorName string, ephemeralFlag, pourFlag bool) (*BondResult, error) {
	// Same as bondProtoMol but with arguments swapped
	return bondProtoMol(ctx, s, proto, mol, bondType, vars, childRef, actorName, ephemeralFlag, pourFlag)
}

// bondMolMol bonds two molecules together
func bondMolMol(ctx context.Context, s storage.Storage, molA, molB *types.Issue, bondType, actorName string) (*BondResult, error) {
	err := s.RunInTransaction(ctx, func(tx storage.Transaction) error {
		// Add dependency: B links to A
		// Sequential: use blocks (B runs after A completes)
		// Conditional: use conditional-blocks (B runs only if A fails)
		// Parallel: use parent-child (organizational, no blocking)
		// Note: Schema only allows one dependency per (issue_id, depends_on_id) pair
		var depType types.DependencyType
		switch bondType {
		case types.BondTypeSequential:
			depType = types.DepBlocks
		case types.BondTypeConditional:
			depType = types.DepConditionalBlocks
		default:
			depType = types.DepParentChild
		}
		dep := &types.Dependency{
			IssueID:     molB.ID,
			DependsOnID: molA.ID,
			Type:        depType,
		}
		if err := tx.AddDependency(ctx, dep, actorName); err != nil {
			return fmt.Errorf("linking molecules: %w", err)
		}

		// Note: bonded_from field tracking is not yet supported by storage layer.
		// The dependency relationship captures the bonding semantics.
		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("linking molecules: %w", err)
	}

	return &BondResult{
		ResultID:   molA.ID,
		ResultType: "compound_molecule",
		BondType:   bondType,
	}, nil
}

// minPriority returns the higher priority (lower number)
func minPriority(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// resolveOrDescribe checks if an operand is an issue or formula without cooking.
// Used for dry-run mode. Returns (issue, formulaName, error).
// If it's an issue, issue is set. If it's a formula, formulaName is set.
func resolveOrDescribe(ctx context.Context, s storage.Storage, operand string) (*types.Issue, string, error) {
	// First, try to resolve as an existing issue
	id, err := utils.ResolvePartialID(ctx, s, operand)
	if err == nil {
		issue, err := s.GetIssue(ctx, id)
		if err == nil {
			return issue, "", nil
		}
	}

	// Not found as issue - check if it looks like a formula name
	if !looksLikeFormulaName(operand) {
		return nil, "", fmt.Errorf("'%s' not found (not an issue ID or formula name)", operand)
	}

	// Try to load the formula (but don't cook it)
	parser := formula.NewParser()
	f, err := parser.LoadByName(operand)
	if err != nil {
		return nil, "", fmt.Errorf("'%s' not found as issue or formula: %w", operand, err)
	}

	return nil, f.Formula, nil
}

// resolveOrCookToSubgraph tries to resolve an operand as an issue ID or formula.
// If it's an issue, loads the subgraph from DB. If it's a formula, cooks inline to subgraph.
// Returns the subgraph, whether it was cooked from formula, and any error.
//
// This implements gt-4v1eo: formulas are cooked to in-memory subgraphs (no DB storage).
func resolveOrCookToSubgraph(ctx context.Context, s storage.Storage, operand string) (*TemplateSubgraph, bool, error) {
	// First, try to resolve as an existing issue
	id, err := utils.ResolvePartialID(ctx, s, operand)
	if err == nil {
		issue, err := s.GetIssue(ctx, id)
		if err == nil {
			// Check if it's a proto (template)
			if isProto(issue) {
				subgraph, err := loadTemplateSubgraph(ctx, s, id)
				if err != nil {
					return nil, false, fmt.Errorf("loading proto subgraph '%s': %w", id, err)
				}
				return subgraph, false, nil
			}
			// It's a molecule, not a proto - wrap it as a single-issue subgraph
			return &TemplateSubgraph{
				Root:     issue,
				Issues:   []*types.Issue{issue},
				IssueMap: map[string]*types.Issue{issue.ID: issue},
			}, false, nil
		}
	}

	// Not found as issue - check if it looks like a formula name
	if !looksLikeFormulaName(operand) {
		return nil, false, fmt.Errorf("'%s' not found (not an issue ID or formula name)", operand)
	}

	// Try to cook formula inline to in-memory subgraph
	subgraph, err := resolveAndCookFormula(operand, nil)
	if err != nil {
		return nil, false, fmt.Errorf("'%s' not found as issue or formula: %w", operand, err)
	}

	return subgraph, true, nil
}

// looksLikeFormulaName checks if an operand looks like a formula name.
// Formula names typically start with "mol-" or contain ".formula" patterns.
func looksLikeFormulaName(operand string) bool {
	// Common formula prefixes
	if strings.HasPrefix(operand, "mol-") {
		return true
	}
	// Formula file references
	if strings.Contains(operand, ".formula") {
		return true
	}
	// If it contains a path separator, might be a formula path
	if strings.Contains(operand, "/") || strings.Contains(operand, "\\") {
		return true
	}
	return false
}

func init() {
	molBondCmd.Flags().String("type", types.BondTypeSequential, "Bond type: sequential, parallel, or conditional")
	molBondCmd.Flags().String("as", "", "Custom title for compound proto (proto+proto only)")
	molBondCmd.Flags().Bool("dry-run", false, "Preview what would be created")
	molBondCmd.Flags().StringSlice("var", []string{}, "Variable substitution for spawned protos (key=value)")
	molBondCmd.Flags().Bool("ephemeral", false, "Force spawn as vapor (ephemeral, Ephemeral=true)")
	molBondCmd.Flags().Bool("pour", false, "Force spawn as liquid (persistent, Ephemeral=false)")
	molBondCmd.Flags().String("ref", "", "Custom child reference with {{var}} substitution (e.g., arm-{{polecat_name}})")

	molCmd.AddCommand(molBondCmd)
}
