package start

import (
	"context"
	"fmt"
	"io"
	"time"

	"github.com/moby/moby/api/pkg/stdcopy"
	"github.com/moby/moby/api/types/container"
	adminv1 "github.com/schmitthub/clawker/api/admin/v1"
	"github.com/schmitthub/clawker/internal/cmd/container/shared"
	"github.com/schmitthub/clawker/internal/cmdutil"
	"github.com/schmitthub/clawker/internal/config"
	"github.com/schmitthub/clawker/internal/controlplane/cpboot"
	"github.com/schmitthub/clawker/internal/docker"
	"github.com/schmitthub/clawker/internal/hostproxy"
	"github.com/schmitthub/clawker/internal/iostreams"
	"github.com/schmitthub/clawker/internal/logger"
	"github.com/schmitthub/clawker/internal/project"
	"github.com/schmitthub/clawker/internal/signals"
	"github.com/schmitthub/clawker/internal/socketbridge"
	"github.com/spf13/cobra"
)

// StartOptions holds options for the start command.
type StartOptions struct {
	IOStreams      *iostreams.IOStreams
	Client         func(context.Context) (*docker.Client, error)
	Config         func() (config.Config, error)
	ProjectManager func() (project.ProjectManager, error)
	HostProxy      func() hostproxy.HostProxyService
	ControlPlane   func() cpboot.Manager
	AdminClient    func(context.Context) (adminv1.AdminServiceClient, error)
	SocketBridge   func() socketbridge.SocketBridgeManager
	Logger         func() (*logger.Logger, error)

	Agent       bool // Use agent name (resolves to clawker.<project>.<agent>)
	Attach      bool
	Containers  []string
	Interactive bool
}

// NewCmdStart creates the container start command.
func NewCmdStart(f *cmdutil.Factory, runF func(context.Context, *StartOptions) error) *cobra.Command {
	opts := &StartOptions{
		IOStreams:      f.IOStreams,
		Client:         f.Client,
		Config:         f.Config,
		ProjectManager: f.ProjectManager,
		HostProxy:      f.HostProxy,
		ControlPlane:   f.ControlPlane,
		AdminClient:    f.AdminClient,
		SocketBridge:   f.SocketBridge,
		Logger:         f.Logger,
	}

	cmd := &cobra.Command{
		Use:   "start [OPTIONS] CONTAINER [CONTAINER...]",
		Short: "Start one or more stopped containers",
		Long: `Starts one or more stopped clawker containers.

When --agent is provided, the container name is resolved as clawker.<project>.<agent>
if you are within a registered (clawker project init) project directory.

Container names can be:
  - Full name: clawker.myproject.myagent
  - Container ID: abc123...`,
		Example: `  # Start a stopped container by full name
  clawker container start clawker.myapp.dev

  # Start a container using agent name (resolves via project config)
  clawker container start --agent dev

  # Start multiple containers
  clawker container start clawker.myapp.dev clawker.myapp.writer

  # Start and attach your terminal
  clawker container start -ia clawker.myapp.dev`,
		Args: cmdutil.RequiresMinArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.Containers = args
			if runF != nil {
				return runF(cmd.Context(), opts)
			}
			return startRun(cmd.Context(), opts)
		},
	}

	cmd.Flags().BoolVar(&opts.Agent, "agent", false, "Use agent name (resolves to clawker.<project>.<agent>)")
	cmd.Flags().BoolVarP(&opts.Attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals")
	cmd.Flags().BoolVarP(&opts.Interactive, "interactive", "i", false, "Attach container's STDIN")

	return cmd
}

func startRun(ctx context.Context, opts *StartOptions) error {
	ctx, cancelFun := context.WithCancel(ctx)
	defer cancelFun()
	ios := opts.IOStreams
	log, err := opts.Logger()
	if err != nil {
		return fmt.Errorf("initializing logger: %w", err)
	}
	cfg, err := opts.Config()
	if err != nil {
		return fmt.Errorf("failed to load config: %w", err)
	}

	// --- Phase A: Config + Docker connect + host proxy ---

	client, err := opts.Client(ctx)
	if err != nil {
		return fmt.Errorf("connecting to Docker: %w", err)
	}

	// Resolve container names if --agent provided
	containers := opts.Containers
	if opts.Agent {
		var projectName string
		if pm, err := opts.ProjectManager(); err == nil {
			if p, err := pm.CurrentProject(ctx); err == nil {
				projectName = p.Name()
			}
		}
		resolved, err := docker.ContainerNamesFromAgents(projectName, containers)
		if err != nil {
			return err
		}
		containers = resolved
	}

	// --- Phase B: Start containers ---

	if opts.Attach || opts.Interactive {
		if len(containers) > 1 {
			return fmt.Errorf("you cannot attach to multiple containers at once. If you want to start multiple containers, do so without --attach or --interactive")
		}

		containerName := containers[0]
		// Bootstrap host services (CP ensure, host proxy, firewall init/rules)
		// under a spinner BEFORE attach. Run in cooked mode here so the spinner
		// stays clear of the raw-tty stream attachAndStart sets up. attachAndStart
		// then handles attach + docker start + post-start without re-running
		// pre-start.
		cmdOpts := shared.CommandOpts{
			Client:         opts.Client,
			Config:         opts.Config,
			ProjectManager: opts.ProjectManager,
			HostProxy:      opts.HostProxy,
			ControlPlane:   opts.ControlPlane,
			AdminClient:    opts.AdminClient,
			SocketBridge:   opts.SocketBridge,
			Logger:         opts.Logger,
		}
		if err := ios.RunWithSpinner("Bootstrapping host services", func() error {
			return shared.BootstrapServicesPreStart(ctx, containerName, cmdOpts)
		}); err != nil {
			return fmt.Errorf("starting container: %w", err)
		}
		return attachAndStart(ctx, ios, log, client, containerName, cfg, cmdOpts, opts)
	}

	// Start all containers without attaching
	return startContainersWithoutAttach(ctx, ios, log, client, containers, cfg, opts)
}

// attachAndStart attaches to a container, starts I/O, then starts the container.
// Follows the canonical attach-then-start pattern from run.go:
//
//	Attach → Wait channel → I/O goroutines → Start → Socket bridge → Resize → Wait
//
// I/O streaming starts pre-start; resize starts post-start. This ensures we're
// ready to receive output immediately and avoids kernel pipe buffer issues.
func attachAndStart(ctx context.Context, ios *iostreams.IOStreams, log *logger.Logger, client *docker.Client, containerName string, cfg config.Config, cmdOpts shared.CommandOpts, opts *StartOptions) error {
	// Find and inspect the container
	c, err := client.FindContainerByName(ctx, containerName)
	if err != nil {
		return fmt.Errorf("failed to find container %q: %w", containerName, err)
	}
	if c == nil {
		return fmt.Errorf("container %q not found", containerName)
	}

	info, err := client.ContainerInspect(ctx, c.ID, docker.ContainerInspectOptions{})
	if err != nil {
		return fmt.Errorf("failed to inspect container: %w", err)
	}

	hasTTY := info.Container.Config.Tty
	containerID := c.ID

	// Create attach options
	attachOpts := docker.ContainerAttachOptions{
		Stream: true,
		Stdin:  opts.Interactive,
		Stdout: true,
		Stderr: true,
	}

	// Set up TTY if the container has it and we're interactive
	var pty *docker.PTYHandler
	if hasTTY && opts.Interactive {
		pty = docker.NewPTYHandler(log)
		if err := pty.Setup(); err != nil {
			return fmt.Errorf("failed to set up terminal: %w", err)
		}
		defer pty.Restore()
	}

	// Attach to container BEFORE starting it.
	// Critical for short-lived containers where the container might exit
	// before we can attach if we start first.
	log.Debug().Msg("attaching to container before start")
	hijacked, err := client.ContainerAttach(ctx, containerID, attachOpts)
	if err != nil {
		log.Debug().Err(err).Msg("container attach failed")
		return fmt.Errorf("attaching to container: %w", err)
	}
	defer hijacked.Close()
	log.Debug().Msg("container attach succeeded")

	// Set up wait channel for container exit following Docker CLI's waitExitOrRemoved pattern.
	// Must use WaitConditionNextExit (not WaitConditionNotRunning) because this is called
	// before the container starts — a "created" container is already not-running.
	log.Debug().Msg("setting up container wait")
	statusCh := waitForContainerExit(ctx, client, containerID, log)

	// Start I/O streaming BEFORE starting the container.
	// This ensures we're ready to receive output immediately when the container starts.
	// Following Docker CLI pattern: I/O goroutines start pre-start, resize happens post-start.
	streamDone := make(chan error, 1)

	if hasTTY && pty != nil {
		// TTY mode: use Stream (I/O only, no resize — resize happens after start)
		go func() {
			streamDone <- pty.Stream(ctx, hijacked.HijackedResponse)
		}()
	} else {
		// Non-TTY mode: demux the multiplexed stream
		go func() {
			_, err := stdcopy.StdCopy(ios.Out, ios.ErrOut, hijacked.Reader)
			streamDone <- err
		}()

		// Copy stdin to container if interactive
		if opts.Interactive {
			go func() {
				io.Copy(hijacked.Conn, ios.In)
				hijacked.CloseWrite()
			}()
		}
	}

	// Pre-start already ran in cooked mode at the call site (startRun) — only
	// docker start + post-start here, silently. clawkerd's in-container
	// init-progress reporter owns the foreground TTY via pty.Stream
	// (which copies hijacked container output to os.Stdout) from this point on.
	log.Debug().Msg("starting container")
	if _, err := client.ContainerStart(ctx, docker.ContainerStartOptions{
		ContainerID: containerID,
		EnsureNetwork: &docker.EnsureNetworkOptions{
			Name: cfg.ClawkerNetwork(),
		},
	}); err != nil {
		log.Debug().Err(err).Msg("container start failed")
		return fmt.Errorf("starting container: %w", err)
	}
	if err := shared.BootstrapServicesPostStart(ctx, containerID, cmdOpts); err != nil {
		return fmt.Errorf("starting container: %w", err)
	}
	log.Debug().Msg("container started successfully")

	// Set up TTY resize AFTER container is running (Docker CLI's MonitorTtySize pattern).
	// The +1/-1 trick forces a SIGWINCH to trigger TUI redraw on re-attach.
	if hasTTY && pty != nil {
		resizeFunc := func(height, width uint) error {
			_, err := client.ContainerResize(ctx, containerID, height, width)
			return err
		}

		if pty.IsTerminal() {
			width, height, err := pty.GetSize()
			if err != nil {
				log.Debug().Err(err).Msg("failed to get initial terminal size")
			} else {
				if err := resizeFunc(uint(height+1), uint(width+1)); err != nil {
					log.Debug().Err(err).Msg("failed to set artificial container TTY size")
				}
				if err := resizeFunc(uint(height), uint(width)); err != nil {
					log.Debug().Err(err).Msg("failed to set actual container TTY size")
				}
			}

			// Monitor for window resize events (SIGWINCH)
			resizeHandler := signals.NewResizeHandler(resizeFunc, pty.GetSize)
			resizeHandler.Start()
			defer resizeHandler.Stop()
		}
	}

	// Wait for stream completion or container exit.
	// Following Docker CLI's run.go pattern: when stream ends, check exit status;
	// when exit status arrives first, drain the stream.
	select {
	case err := <-streamDone:
		log.Debug().Err(err).Msg("stream completed")
		if err != nil {
			return err
		}
		// Stream done — check for container exit status.
		// For normal container exits, the status is available almost immediately.
		// For detach (Ctrl+P Ctrl+Q), the container is still running so no status
		// arrives. We use a timeout to distinguish the two cases without blocking
		// forever. This is necessary because we don't do client-side detach key
		// detection (Docker CLI uses term.EscapeError for this).
		select {
		case wr := <-statusCh:
			log.Debug().Int("exitCode", wr.exitCode).Err(wr.err).Msg("container exited")
			if wr.err != nil {
				return wr.err
			}
			if wr.exitCode != 0 {
				return &cmdutil.ExitError{Code: wr.exitCode}
			}
			return nil
		case <-time.After(2 * time.Second):
			// No exit status within timeout — stream ended due to detach, not exit.
			log.Debug().Msg("no exit status received after stream ended, assuming detach")
			return nil
		}
	case wr := <-statusCh:
		log.Debug().Int("exitCode", wr.exitCode).Err(wr.err).Msg("container exited before stream completed")
		// Wait for stream to finish flushing buffered output.
		// Docker CLI does the same: "we need to keep the streamer running
		// until all output is read." The daemon closes the hijacked connection
		// on container exit, so the stream goroutine terminates via EOF.
		<-streamDone
		if wr.err != nil {
			return wr.err
		}
		if wr.exitCode != 0 {
			return &cmdutil.ExitError{Code: wr.exitCode}
		}
		return nil
	}
}

// waitResult carries the container exit code and any error from the wait.
type waitResult struct {
	exitCode int
	err      error // non-nil if the wait itself failed (distinct from non-zero exit code)
}

// waitForContainerExit wraps ContainerWait into a single result channel.
// Always uses WaitConditionNextExit (start hasn't happened yet, and start
// command never has autoRemove).
func waitForContainerExit(ctx context.Context, client *docker.Client, containerID string, log *logger.Logger) <-chan waitResult {
	ch := make(chan waitResult, 1)
	go func() {
		defer close(ch)
		wr := client.ContainerWait(ctx, containerID, container.WaitConditionNextExit)
		select {
		case <-ctx.Done():
			return
		case result := <-wr.Result:
			if result.Error != nil {
				log.Error().Str("message", result.Error.Message).Msg("container wait error")
				ch <- waitResult{exitCode: 125, err: fmt.Errorf("container wait error: %s", result.Error.Message)}
			} else {
				ch <- waitResult{exitCode: int(result.StatusCode)}
			}
		case err := <-wr.Error:
			log.Error().Err(err).Msg("error waiting for container")
			ch <- waitResult{exitCode: 125, err: fmt.Errorf("waiting for container: %w", err)}
		}
	}()
	return ch
}

// startContainersWithoutAttach starts multiple containers without attaching.
func startContainersWithoutAttach(ctx context.Context, ios *iostreams.IOStreams, log *logger.Logger, client *docker.Client, containers []string, cfg config.Config, opts *StartOptions) error {
	var errs []error
	for _, name := range containers {
		_, err := shared.ContainerStart(ctx,
			shared.CommandOpts{
				Client:         opts.Client,
				Config:         opts.Config,
				ProjectManager: opts.ProjectManager,
				HostProxy:      opts.HostProxy,
				ControlPlane:   opts.ControlPlane,
				AdminClient:    opts.AdminClient,
				SocketBridge:   opts.SocketBridge,
				Logger:         opts.Logger,
			},
			docker.ContainerStartOptions{
				ContainerID: name,
				EnsureNetwork: &docker.EnsureNetworkOptions{
					Name: cfg.ClawkerNetwork(),
				},
			})
		if err != nil {
			cs := ios.ColorScheme()
			fmt.Fprintf(ios.ErrOut, "%s Failed to start %s: %v\n", cs.FailureIcon(), name, err)
			errs = append(errs, fmt.Errorf("failed to start %s: %w", name, err))
		} else {
			// Print container name on success
			fmt.Fprintln(ios.Out, name)
		}
	}

	if len(errs) > 0 {
		return cmdutil.SilentError
	}

	return nil
}
