package workspace

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/client"
	"github.com/google/uuid"
	"github.com/uptrace/bun"
	"net/url"
	"regexp"
	"sync"
	"tesseract/internal/docker"
	"tesseract/internal/reverseproxy"
	"tesseract/internal/service"
)

type workspace struct {
	bun.BaseModel `bun:"table:workspaces,alias:workspace"`

	ID uuid.UUID `bun:",type:uuid,pk"`

	Name string `json:"name"`

	// containerId is the ID of the docker container
	ContainerID string `json:"containerId"`

	ImageTag string `json:"imageTag"`

	CreatedAt string `json:"createdAt"`

	SSHPort int `bun:"-" json:"sshPort,omitempty"`

	Status status `bun:"-" json:"status"`

	PortMappings []portMapping `bun:"rel:has-many,join:id=workspace_id" json:"ports,omitempty"`

	Runtime string `json:"runtime"`
}

type portMapping struct {
	bun.BaseModel `bun:"table:port_mappings,alias:port_mapping"`

	WorkspaceID   uuid.UUID `bun:",type:uuid,pk" json:"-"`
	ContainerPort int       `json:"port"`
	Subdomain     string    `json:"subdomain"`

	Workspace workspace `bun:"rel:belongs-to,join:workspace_id=id" json:"-"`
}

type workspaceRuntime struct {
	Name string `json:"name"`
	Path string `json:"path"`
}

// status represents the status of a workspace.
type status string

const (
	statusRunning    status = "running"
	statusStopped    status = "stopped"
	statusPaused     status = "paused"
	statusRestarting status = "restarting"
	statusUnknown    status = "unknown"
)

var workspaceNameRegex = regexp.MustCompile("^[\\w-]+$")

func SyncAll(ctx context.Context, services service.Services) error {
	tx, err := services.Database.BeginTx(ctx, nil)
	if err != nil {
		return err
	}

	var workspaces []workspace
	if err = tx.NewSelect().Model(&workspaces).
		Column("id", "container_id").
		Scan(ctx); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil
		}
	}
	if len(workspaces) == 0 {
		return nil
	}

	var wg sync.WaitGroup
	var mu sync.Mutex
	var errs []error

	var deletedWorkspaces []workspace

	for _, w := range workspaces {
		w := w
		wg.Add(1)
		go func() {
			var err error
			defer wg.Done()
			defer func() {
				mu.Lock()
				errs = append(errs, err)
				mu.Unlock()
			}()

			if err = services.DockerClient.ContainerStart(ctx, w.ContainerID, container.StartOptions{}); err != nil {
				if client.IsErrNotFound(err) {
					err = nil
					mu.Lock()
					deletedWorkspaces = append(deletedWorkspaces, w)
					mu.Unlock()
				}
				return
			}

			inspect, err := services.DockerClient.ContainerInspect(ctx, w.ContainerID)
			if err != nil {
				return
			}

			internalPort := docker.ContainerSSHHostPort(ctx, inspect)
			if internalPort <= 0 {
				return
			}

			err = services.SSHProxy.NewProxyEntryTo(internalPort)
		}()
	}

	wg.Wait()

	if err = errors.Join(errs...); err != nil {
		_ = tx.Rollback()
		return err
	}

	if len(deletedWorkspaces) > 0 {
		_, err = tx.NewDelete().Model(&deletedWorkspaces).WherePK().Exec(ctx)
		if err != nil {
			_ = tx.Rollback()
			return err
		}
	}

	if err = tx.Commit(); err != nil {
		_ = tx.Rollback()
		return err
	}

	if err = initializeHTTPProxies(ctx, services.Database, services.DockerClient, services.ReverseProxy); err != nil {
		return err
	}

	return nil
}

func initializeHTTPProxies(ctx context.Context, db *bun.DB, dockerClient *client.Client, proxy *reverseproxy.ReverseProxy) error {
	var mappings []portMapping
	if err := db.NewSelect().
		Model(&mappings).
		Relation("Workspace", func(q *bun.SelectQuery) *bun.SelectQuery {
			return q.Column("container_id")
		}).
		Scan(ctx); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil
		}
		return err
	}

	if len(mappings) == 0 {
		return nil
	}

	var wg sync.WaitGroup
	var errs []error
	var mu sync.Mutex

	for _, m := range mappings {
		m := m
		wg.Add(1)
		go func() {
			var err error
			defer wg.Done()
			defer func() {
				mu.Lock()
				errs = append(errs, err)
				mu.Unlock()
			}()

			inspect, err := dockerClient.ContainerInspect(ctx, m.Workspace.ContainerID)
			if err != nil {
				return
			}

			u, err := url.Parse(fmt.Sprintf("http://%s:%d", inspect.NetworkSettings.IPAddress, m.ContainerPort))
			if err != nil {
				return
			}

			proxy.AddEntry(m.Subdomain, u)
		}()
	}

	wg.Wait()

	if err := errors.Join(errs...); err != nil {
		return err
	}

	return nil
}
