package pipelines

import (
	"bytes"
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"time"

	"tangled.org/core/api/tangled"
	"tangled.org/core/appview/config"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/middleware"
	"tangled.org/core/appview/oauth"
	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/reporesolver"
	"tangled.org/core/hostutil"
	"tangled.org/core/idresolver"
	"tangled.org/core/orm"
	"tangled.org/core/rbac"
	spindlemodel "tangled.org/core/spindle/models"

	"github.com/bluesky-social/indigo/atproto/syntax"
	indigoxrpc "github.com/bluesky-social/indigo/xrpc"
	"github.com/go-chi/chi/v5"
	"github.com/gorilla/websocket"
)

type Pipelines struct {
	repoResolver *reporesolver.RepoResolver
	idResolver   *idresolver.Resolver
	config       *config.Config
	oauth        *oauth.OAuth
	pages        *pages.Pages
	db           *db.DB
	enforcer     *rbac.Enforcer
	logger       *slog.Logger
}

func (p *Pipelines) Router(mw *middleware.Middleware) http.Handler {
	r := chi.NewRouter()
	r.Get("/", p.Index)
	r.Get("/{pipeline}/workflow/{workflow}", p.Workflow)
	r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs)
	r.
		With(mw.RepoPermissionMiddleware("repo:owner")).
		Post("/{pipeline}/workflow/{workflow}/cancel", p.CancelWorkflow)

	return r
}

func New(
	oauth *oauth.OAuth,
	repoResolver *reporesolver.RepoResolver,
	pages *pages.Pages,
	idResolver *idresolver.Resolver,
	db *db.DB,
	config *config.Config,
	enforcer *rbac.Enforcer,
	logger *slog.Logger,
) *Pipelines {
	return &Pipelines{
		oauth:        oauth,
		repoResolver: repoResolver,
		pages:        pages,
		idResolver:   idResolver,
		config:       config,
		db:           db,
		enforcer:     enforcer,
		logger:       logger,
	}
}

func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
	user := p.oauth.GetMultiAccountUser(r)
	l := p.logger.With("handler", "Index")

	f, err := p.repoResolver.Resolve(r)
	if err != nil {
		l.Error("failed to get repo and knot", "err", err)
		return
	}

	filterKind := r.URL.Query().Get("trigger")
	filters := []orm.Filter{
		orm.FilterEq("p.repo_did", f.RepoDid),
	}
	switch filterKind {
	case "push":
		filters = append(filters, orm.FilterEq("t.kind", "push"))
	case "pull_request":
		filters = append(filters, orm.FilterEq("t.kind", "pull_request"))
	default:
		// no filters otherwise, default to "all"
		filterKind = "all"
	}

	// sh.tangled.ci.queryPipelines(repo, kind, limit=30)
	xrpcc := indigoxrpc.Client{Host: f.Spindle}
	out, err := tangled.CiQueryPipelines(r.Context(), &xrpcc, nil, "", 1, f.RepoDid)
	if err != nil {
		l.Error("failed to fetch pipelines", "err", err)
		panic("unimplemented") // spindle failure, appview should not fail.
	}

	p.pages.Pipelines(w, pages.PipelinesParams{
		BaseParams: pages.BaseParamsFromContext(r.Context()),
		RepoInfo:   p.repoResolver.GetRepoInfo(r, user),
		Pipelines:  out.Pipelines,
		FilterKind: filterKind,
		Total:      out.Total,
	})
}

func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) {
	user := p.oauth.GetMultiAccountUser(r)
	l := p.logger.With("handler", "Workflow")

	f, err := p.repoResolver.Resolve(r)
	if err != nil {
		l.Error("failed to get repo and knot", "err", err)
		p.pages.Error404(w)
		return
	}

	pipelineId, err := syntax.ParseTID(chi.URLParam(r, "pipeline"))
	if err != nil {
		l.Debug("invalid pipeline id", "id", pipelineId)
		p.pages.Error404(w)
		return
	}

	workflowName := chi.URLParam(r, "workflow")
	if workflowName == "" {
		l.Debug("empty workflow name")
		p.pages.Error404(w)
		return
	}

	l = l.With("pipeline", pipelineId, "workflow", workflowName)

	// TODO: change url path to:
	// /{owner}/{slug}/pipelines/{spindle}/{pipeline(tid)}/workflow/{workflow}
	// do we want spindle DID or just Hostname? <- DID will make more sense since it's not defined in DID doc.

	xrpcc := &indigoxrpc.Client{Host: f.Spindle}
	out, err := tangled.CiGetPipeline(r.Context(), xrpcc, pipelineId.String())
	if err != nil {
		// TODO(boltless): change behavior based on error
		l.Debug("failed to get pipeline", "err", err)
		p.pages.Error404(w)
		return
	}

	// ensure workflow exists
	exist := false
	for _, workflow := range out.Workflows {
		if workflow.Name == workflowName {
			exist = true
			break
		}
	}
	if !exist {
		l.Debug("workflow doesn't exist in pipeline")
		p.pages.Error404(w)
		return
	}

	p.pages.Workflow(w, pages.WorkflowParams{
		BaseParams: pages.BaseParamsFromContext(r.Context()),
		RepoInfo:   p.repoResolver.GetRepoInfo(r, user),
		Pipeline:   out,
		Workflow:   workflowName,
	})
}

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

func (p *Pipelines) Logs(w http.ResponseWriter, r *http.Request) {
	l := p.logger.With("handler", "logs")

	f, err := p.repoResolver.Resolve(r)
	if err != nil {
		l.Error("failed to get repo and knot", "err", err)
		http.Error(w, "bad repo/knot", http.StatusBadRequest)
		return
	}

	if f.Spindle == "" {
		http.Error(w, "invalid repo info", http.StatusBadRequest)
		return
	}

	pipelineId, err := syntax.ParseTID(chi.URLParam(r, "pipeline"))
	if err != nil {
		l.Debug("invalid pipeline id", "id", pipelineId)
		http.Error(w, "invalid pipeline id", http.StatusBadRequest)
		return
	}

	workflowName := chi.URLParam(r, "workflow")
	if workflowName == "" {
		l.Debug("empty workflow name")
		http.Error(w, "invalid workflow name", http.StatusBadRequest)
		return
	}

	logsUrl := SubscribeLogsUrl(f.Spindle, pipelineId, workflowName)
	if logsUrl == "" {
		http.Error(w, "invalid spindle hostname", http.StatusBadRequest)
		return
	}
	l = l.With("url", logsUrl)

	clientConn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		l.Error("websocket upgrade failed", "err", err)
		return
	}
	defer func() {
		_ = clientConn.WriteControl(
			websocket.CloseMessage,
			websocket.FormatCloseMessage(websocket.CloseNormalClosure, "log stream complete"),
			time.Now().Add(time.Second),
		)
		clientConn.Close()
	}()

	ctx, cancel := context.WithCancel(r.Context())
	defer cancel()

	l.Info("logs endpoint hit")

	spindleConn, _, err := websocket.DefaultDialer.Dial(logsUrl, nil)
	if err != nil {
		l.Error("websocket dial failed", "err", err)
		return
	}
	defer spindleConn.Close()

	// create a channel for incoming messages
	evChan := make(chan LogEvent, 100)
	// start a goroutine to read from spindle
	go ReadLogs(spindleConn, evChan)

	stepStartTimes := make(map[int]time.Time)
	stepAnsi := make(map[int]*ansiState)
	var fragment bytes.Buffer
	for {
		select {
		case <-ctx.Done():
			l.Info("client disconnected")
			return

		case ev, ok := <-evChan:
			if !ok {
				continue
			}

			if ev.Err != nil && ev.IsCloseError() {
				l.Debug("graceful shutdown, tail complete", "err", err)
				return
			}
			if ev.Err != nil {
				l.Error("error reading from spindle", "err", err)
				return
			}

			var logLine spindlemodel.LogLine
			if err = json.Unmarshal(ev.Msg, &logLine); err != nil {
				l.Error("failed to parse logline", "err", err)
				continue
			}

			fragment.Reset()

			switch logLine.Kind {
			case spindlemodel.LogKindControl:
				switch logLine.StepStatus {
				case spindlemodel.StepStatusStart:
					stepStartTimes[logLine.StepId] = logLine.Time
					collapsed := false
					if logLine.StepKind == spindlemodel.StepKindSystem {
						collapsed = true
					}
					err = p.pages.LogBlock(&fragment, pages.LogBlockParams{
						Id:        logLine.StepId,
						Name:      logLine.Content,
						Command:   logLine.StepCommand,
						Collapsed: collapsed,
						StartTime: logLine.Time,
					})
				case spindlemodel.StepStatusEnd:
					startTime := stepStartTimes[logLine.StepId]
					endTime := logLine.Time
					err = p.pages.LogBlockEnd(&fragment, pages.LogBlockEndParams{
						Id:        logLine.StepId,
						StartTime: startTime,
						EndTime:   endTime,
					})
				}

			case spindlemodel.LogKindData:
				ansi, ok := stepAnsi[logLine.StepId]
				if !ok {
					ansi = NewAnsiState()
					stepAnsi[logLine.StepId] = ansi
				}
				err = p.pages.LogLine(&fragment, pages.LogLineParams{
					Id:      logLine.StepId,
					Content: ansi.Render(logLine.Content),
				})
			}
			if err != nil {
				l.Error("failed to render log line", "err", err)
				return
			}

			if err = clientConn.WriteMessage(websocket.TextMessage, fragment.Bytes()); err != nil {
				l.Error("error writing to client", "err", err)
				return
			}

		case <-time.After(30 * time.Second):
			l.Debug("sent keepalive")
			if err = clientConn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil {
				l.Error("failed to write control", "err", err)
				return
			}
		}
	}
}

func (p *Pipelines) CancelWorkflow(w http.ResponseWriter, r *http.Request) {
	l := p.logger.With("handler", "CancelWorkflow")
	errorId := "workflow-error"

	f, err := p.repoResolver.Resolve(r)
	if err != nil {
		l.Error("failed to get repo and knot", "err", err)
		p.pages.Notice(w, errorId, "Failed to cancel workflow")
		return
	}
	l = l.With("repo", f.RepoDid)

	if f.Spindle == "" {
		l.Debug("spindle is empty")
		p.pages.Notice(w, errorId, "Failed to cancel workflow")
		return
	}

	pipelineId, err := syntax.ParseTID(chi.URLParam(r, "pipeline"))
	if err != nil {
		l.Debug("invalid pipeline id", "id", pipelineId)
		p.pages.Error404(w)
		return
	}

	workflowName := chi.URLParam(r, "workflow")
	if workflowName == "" {
		l.Debug("empty workflow name")
		p.pages.Error404(w)
		return
	}

	l = l.With("pipeline", pipelineId, "workflow", workflowName)

	hostname, noTLS, err := hostutil.ParseHostname(f.Spindle)
	if err != nil {
		http.Error(w, "invalid spindle hostname", http.StatusBadRequest)
		return
	}

	spindleClient, err := p.oauth.ServiceClient(
		r,
		oauth.WithService(hostname),
		oauth.WithLxm(tangled.PipelineCancelPipelineNSID),
		oauth.WithDev(noTLS),
		oauth.WithTimeout(time.Second*30), // workflow cleanup usually takes time
	)

	if err := tangled.PipelineCancelPipeline(
		r.Context(),
		spindleClient,
		&tangled.PipelineCancelPipeline_Input{
			Repo:     string(f.RepoAt()),
			Pipeline: pipelineId.String(),
			Workflow: workflowName,
		},
	); err != nil {
		l.Error("failed to cancel workflow", "err", err)
		p.pages.Notice(w, errorId, "Failed to cancel workflow")
		return
	}
	l.Debug("canceled workflow")
}
