package controllers

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"strings"
	"sync"

	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	tea "github.com/charmbracelet/bubbletea"
	bus "github.com/lmika/events"
	"github.com/pkg/errors"
	"lmika.dev/cmd/dynamo-browse/internal/common/ui/events"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrcodec"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/attrutils"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/queryexpr"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models/serialisable"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/inputhistory"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/itemrenderer"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/services/viewsnapshot"
)

type resultSetUpdateOp int

const (
	resultSetUpdateInit resultSetUpdateOp = iota
	resultSetUpdateQuery
	resultSetUpdateFilter
	resultSetUpdateSnapshotRestore
	resultSetUpdateRescan
	resultSetUpdateTouch
	resultSetUpdateNextPage
	resultSetUpdateScript
	resultSetUpdateResort
)

type MarkOp int

const (
	MarkOpMark MarkOp = iota
	MarkOpUnmark
	MarkOpToggle
)

const (
	queryInputHistoryCategory  = "queries"
	filterInputHistoryCategory = "filters"
)

type TableReadController struct {
	tableService        TableReadService
	workspaceService    *viewsnapshot.ViewSnapshotService
	itemRendererService *itemrenderer.Service
	jobController       *JobsController
	inputHistoryService *inputhistory.Service
	eventBus            *bus.Bus
	tableName           string
	loadFromLastView    bool
	pasteboardProvider  services.PasteboardProvider

	// state
	mutex *sync.Mutex
	state *State
}

func NewTableReadController(
	state *State,
	tableService TableReadService,
	workspaceService *viewsnapshot.ViewSnapshotService,
	itemRendererService *itemrenderer.Service,
	jobController *JobsController,
	inputHistoryService *inputhistory.Service,
	eventBus *bus.Bus,
	pasteboardProvider services.PasteboardProvider,
	tableName string,
) *TableReadController {
	return &TableReadController{
		state:               state,
		tableService:        tableService,
		workspaceService:    workspaceService,
		itemRendererService: itemRendererService,
		jobController:       jobController,
		inputHistoryService: inputHistoryService,
		eventBus:            eventBus,
		tableName:           tableName,
		pasteboardProvider:  pasteboardProvider,
		mutex:               new(sync.Mutex),
	}
}

// Init does an initial scan of the table.  If no table is specified, it prompts for a table, then does a scan.
func (c *TableReadController) Init() tea.Msg {
	// Restore previous view
	if c.loadFromLastView {
		if vs, err := c.workspaceService.ViewRestore(); err == nil && vs != nil {
			return c.updateViewToSnapshot(vs)
		}
	}

	if c.tableName == "" {
		return c.ListTables(true)
	} else {
		return c.ScanTable(c.tableName)
	}
}

func (c *TableReadController) ListTables(quitIfNoTable bool) tea.Msg {
	return NewJob(c.jobController, "Listing tables…", func(ctx context.Context) (any, error) {
		tables, err := c.tableService.ListTables(context.Background())
		if err != nil {
			return nil, err
		}
		return tables, nil
	}).OnDone(func(res any) tea.Msg {
		return PromptForTableMsg{
			Tables: res.([]string),
			OnSelected: func(tableName string) tea.Msg {
				if tableName == "" {
					if quitIfNoTable {
						return tea.Quit()
					}
					return events.StatusMsg("No table selected")
				}

				return c.ScanTable(tableName)
			},
		}
	}).Submit()
}

func (c *TableReadController) ScanTable(name string) tea.Msg {
	return NewJob(c.jobController, "Scanning…", func(ctx context.Context) (*models.ResultSet, error) {
		tableInfo, err := c.tableService.Describe(ctx, name)
		if err != nil {
			return nil, errors.Wrapf(err, "cannot describe %v", c.tableName)
		}

		resultSet, err := c.tableService.Scan(ctx, tableInfo)
		if resultSet != nil {
			resultSet = c.tableService.Filter(resultSet, c.state.Filter())
		}

		return resultSet, err
	}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), true, false, resultSetUpdateInit)).Submit()
}

func (c *TableReadController) SortResultSet(newCriteria models.SortCriteria) {
	c.state.withResultSet(func(rs *models.ResultSet) {
		rs.Sort(newCriteria.Append(models.PKSKSortFilter(rs.TableInfo)))
	})
	c.eventBus.Fire(newResultSetEvent, c.state.resultSet, resultSetUpdateResort)
}

func (c *TableReadController) PromptForQuery() tea.Msg {
	return events.PromptForInputMsg{
		Prompt:  "query: ",
		History: c.inputHistoryService.Iter(context.Background(), queryInputHistoryCategory),
		OnDone: func(value string) tea.Msg {
			resultSet := c.state.ResultSet()
			if resultSet == nil {
				return events.StatusMsg("Result-set is nil")
			}

			var q *queryexpr.QueryExpr
			if value != "" {
				var err error
				q, err = queryexpr.Parse(value)
				if err != nil {
					return events.Error(err)
				}
			}

			return c.runQuery(resultSet.TableInfo, q, "", true, nil)
		},
	}
}

func (c *TableReadController) RunQuery(q *queryexpr.QueryExpr, table *models.TableInfo) tea.Msg {
	return c.runQuery(table, q, "", true, nil)
}

func (c *TableReadController) runQuery(
	tableInfo *models.TableInfo,
	query *queryexpr.QueryExpr,
	newFilter string,
	pushSnapshot bool,
	exclusiveStartKey map[string]types.AttributeValue,
) tea.Msg {
	if query == nil {
		return NewJob(c.jobController, "Scanning…", func(ctx context.Context) (*models.ResultSet, error) {
			newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, nil, exclusiveStartKey)

			if newResultSet != nil && newFilter != "" {
				newResultSet = c.tableService.Filter(newResultSet, newFilter)
			}

			return newResultSet, err
		}).OnEither(c.handleResultSetFromJobResult(newFilter, pushSnapshot, false, resultSetUpdateQuery)).Submit()
	}

	return c.doIfNoneDirty(func() tea.Msg {
		return NewJob(c.jobController, "Running query…", func(ctx context.Context) (*models.ResultSet, error) {
			newResultSet, err := c.tableService.ScanOrQuery(context.Background(), tableInfo, query, exclusiveStartKey)

			if newFilter != "" && newResultSet != nil {
				newResultSet = c.tableService.Filter(newResultSet, newFilter)
			}
			return newResultSet, err
		}).OnEither(c.handleResultSetFromJobResult(newFilter, pushSnapshot, false, resultSetUpdateQuery)).Submit()
	})
}

func (c *TableReadController) doIfNoneDirty(cmd tea.Cmd) tea.Msg {
	var anyDirty = false
	for i := 0; i < len(c.state.ResultSet().Items()); i++ {
		anyDirty = anyDirty || c.state.ResultSet().IsDirty(i)
	}

	if !anyDirty {
		return cmd()
	}

	return events.PromptForInputMsg{
		Prompt: "reset modified items? ",
		OnDone: func(value string) tea.Msg {
			if value != "y" {
				return events.StatusMsg("operation aborted")
			}

			return cmd()
		},
	}
}

func (c *TableReadController) Rescan() tea.Msg {
	return c.doIfNoneDirty(func() tea.Msg {
		resultSet := c.state.ResultSet()
		return c.doScan(resultSet, resultSet.Query, true, resultSetUpdateRescan)
	})
}

func (c *TableReadController) doScan(resultSet *models.ResultSet, query models.Queryable, pushBackstack bool, op resultSetUpdateOp) tea.Msg {
	return NewJob(c.jobController, "Rescan…", func(ctx context.Context) (*models.ResultSet, error) {
		newResultSet, err := c.tableService.ScanOrQuery(ctx, resultSet.TableInfo, query, resultSet.LastEvaluatedKey)
		if newResultSet != nil {
			newResultSet = c.tableService.Filter(newResultSet, c.state.Filter())
		}

		return newResultSet, err
	}).OnEither(c.handleResultSetFromJobResult(c.state.Filter(), pushBackstack, false, op)).Submit()
}

func (c *TableReadController) setResultSetAndFilter(resultSet *models.ResultSet, filter string, pushBackstack bool, op resultSetUpdateOp) tea.Msg {
	if resultSet != nil && pushBackstack {
		details := serialisable.ViewSnapshotDetails{
			TableName: resultSet.TableInfo.Name,
			Filter:    filter,
		}

		if q := resultSet.Query; q != nil {
			if bs, err := q.SerializeToBytes(); err == nil {
				details.Query = bs
				details.QueryHash = q.HashCode()
			} else {
				log.Printf("cannot serialize query to bytes: %v", err)
			}
		}

		if len(resultSet.ExclusiveStartKey) > 0 {
			var err error
			details.ExclusiveStartKey, err = attrcodec.SerializeMapToBytes(resultSet.ExclusiveStartKey)
			if err != nil {
				log.Printf("cannot serialize last evaluated key to byte: %v", err)
			}
		}

		log.Printf("pushing to backstack: table = %v, filter = %v, query_hash = %v",
			details.TableName, details.Filter, details.QueryHash)
		if err := c.workspaceService.PushSnapshot(details); err != nil {
			log.Printf("cannot push snapshot: %v", err)
		}
	}

	c.state.setResultSetAndFilter(resultSet, filter)

	c.eventBus.Fire(newResultSetEvent, resultSet, op)

	return c.state.buildNewResultSetMessage("")
}

func (c *TableReadController) SetResultSet(resultSet *models.ResultSet) tea.Msg {
	c.state.setResultSetAndFilter(resultSet, "")
	c.eventBus.Fire(newResultSetEvent, resultSet, resultSetUpdateScript)
	return c.state.buildNewResultSetMessage("")
}

func (c *TableReadController) Mark(op MarkOp, where string) tea.Msg {
	var (
		whereExpr *queryexpr.QueryExpr
		err       error
	)

	if where != "" {
		whereExpr, err = queryexpr.Parse(where)
		if err != nil {
			return events.Error(err)
		}
	}

	if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
		for i, item := range resultSet.Items() {
			if resultSet.Hidden(i) {
				continue
			}

			if whereExpr != nil {
				res, err := whereExpr.EvalItem(item)
				if err != nil {
					return errors.Wrapf(err, "item %d", i)
				}
				if !attrutils.Truthy(res) {
					continue
				}
			}

			switch op {
			case MarkOpMark:
				resultSet.SetMark(i, true)
			case MarkOpUnmark:
				resultSet.SetMark(i, false)
			case MarkOpToggle:
				resultSet.SetMark(i, !resultSet.Marked(i))
			}
		}
		return nil
	}); err != nil {
		return events.Error(err)
	}
	return events.ResultSetUpdated{}
}

func (c *TableReadController) PromptForFilter() tea.Msg {
	return events.PromptForInputMsg{
		Prompt:  "filter: ",
		History: c.inputHistoryService.Iter(context.Background(), filterInputHistoryCategory),
		OnDone: func(value string) tea.Msg {
			return c.Filter(value)
		},
	}
}

func (c *TableReadController) Filter(value string) tea.Msg {
	resultSet := c.state.ResultSet()
	if resultSet == nil {
		return events.StatusMsg("Result-set is nil")
	}

	return NewJob(c.jobController, "Applying Filter…", func(ctx context.Context) (*models.ResultSet, error) {
		newResultSet := c.tableService.Filter(resultSet, value)
		return newResultSet, nil
	}).OnEither(c.handleResultSetFromJobResult(value, true, false, resultSetUpdateFilter)).Submit()
}

func (c *TableReadController) handleResultSetFromJobResult(
	filter string,
	pushbackStack, errIfEmpty bool,
	op resultSetUpdateOp,
) func(newResultSet *models.ResultSet, err error) tea.Msg {
	return func(newResultSet *models.ResultSet, err error) tea.Msg {
		if err == nil {
			if errIfEmpty && newResultSet.NoResults() {
				return events.StatusMsg("No more results")
			}

			return c.setResultSetAndFilter(newResultSet, filter, pushbackStack, op)
		}

		var partialResultsErr models.PartialResultsError
		if errors.As(err, &partialResultsErr) {
			if newResultSet == nil {
				return events.StatusMsg("Operation cancelled")
			}

			return events.Confirm(applyToN("View the ", len(newResultSet.Items()), "item", "items", " returned so far? "), func(yes bool) tea.Msg {
				if yes {
					return c.setResultSetAndFilter(newResultSet, filter, pushbackStack, op)
				}
				return events.StatusMsg("Operation cancelled")
			})
		}

		if newResultSet != nil {
			return c.setResultSetAndFilter(newResultSet, filter, pushbackStack, op)
		}
		return events.Error(err)
	}
}

func (c *TableReadController) ViewBack() tea.Msg {
	viewSnapshot, err := c.workspaceService.ViewBack()
	if err != nil {
		return events.Error(err)
	} else if viewSnapshot == nil {
		return events.StatusMsg("Backstack is empty")
	}

	log.Printf("view back: table = %v, filter = %v, query_hash = %v",
		viewSnapshot.Details.TableName, viewSnapshot.Details.Filter, viewSnapshot.Details.QueryHash)
	return c.updateViewToSnapshot(viewSnapshot)
}

func (c *TableReadController) ViewForward() tea.Msg {
	viewSnapshot, err := c.workspaceService.ViewForward()
	if err != nil {
		return events.Error(err)
	} else if viewSnapshot == nil {
		return events.StatusMsg("At top of view stack")
	}

	return c.updateViewToSnapshot(viewSnapshot)
}

func (c *TableReadController) NextPage() tea.Msg {
	resultSet := c.state.ResultSet()
	if resultSet == nil {
		return events.StatusMsg("Result-set is nil")
	} else if !resultSet.HasNextPage() {
		return events.StatusMsg("No more results")
	}
	currentFilter := c.state.filter

	return NewJob(c.jobController, "Fetching next page…", func(ctx context.Context) (*models.ResultSet, error) {
		return c.tableService.NextPage(ctx, resultSet)
	}).OnEither(c.handleResultSetFromJobResult(currentFilter, true, true, resultSetUpdateNextPage)).Submit()
}

func (c *TableReadController) updateViewToSnapshot(viewSnapshot *serialisable.ViewSnapshot) tea.Msg {
	var err error
	currentResultSet := c.state.ResultSet()

	var query *queryexpr.QueryExpr
	if len(viewSnapshot.Details.Query) > 0 {
		query, err = queryexpr.DeserializeFrom(bytes.NewReader(viewSnapshot.Details.Query))
		if err != nil {
			return err
		}
	}

	var exclusiveStartKey map[string]types.AttributeValue
	if len(viewSnapshot.Details.ExclusiveStartKey) > 0 {
		exclusiveStartKey, err = attrcodec.DeseralizedMapFromBytes(viewSnapshot.Details.ExclusiveStartKey)
		if err != nil {
			return err
		}
	}

	if currentResultSet == nil {
		return NewJob(c.jobController, "Fetching table info…", func(ctx context.Context) (*models.TableInfo, error) {
			tableInfo, err := c.tableService.Describe(context.Background(), viewSnapshot.Details.TableName)
			if err != nil {
				return nil, err
			}
			return tableInfo, nil
		}).OnDone(func(tableInfo *models.TableInfo) tea.Msg {
			return c.runQuery(tableInfo, query, viewSnapshot.Details.Filter, false, exclusiveStartKey)
		}).Submit()
	}

	queryEqualsCurrentQuery := false
	if q, ok := currentResultSet.Query.(*queryexpr.QueryExpr); ok && q != nil {
		queryEqualsCurrentQuery = q.Equal(query)
	}

	if viewSnapshot.Details.TableName == currentResultSet.TableInfo.Name && queryEqualsCurrentQuery {
		return NewJob(c.jobController, "Applying filter…", func(ctx context.Context) (*models.ResultSet, error) {
			return c.tableService.Filter(currentResultSet, viewSnapshot.Details.Filter), nil
		}).OnEither(c.handleResultSetFromJobResult(viewSnapshot.Details.Filter, false, false, resultSetUpdateSnapshotRestore)).Submit()
	}

	return NewJob(c.jobController, "Running query…", func(ctx context.Context) (tea.Msg, error) {
		tableInfo := currentResultSet.TableInfo
		if viewSnapshot.Details.TableName != currentResultSet.TableInfo.Name {
			tableInfo, err = c.tableService.Describe(context.Background(), viewSnapshot.Details.TableName)
			if err != nil {
				return nil, err
			}
		}

		return c.runQuery(tableInfo, query, viewSnapshot.Details.Filter, false, exclusiveStartKey), nil
	}).OnDone(func(m tea.Msg) tea.Msg {
		return m
	}).Submit()
}

func (c *TableReadController) CopyItemToClipboard(idx int) tea.Msg {
	itemCount := 0
	if err := c.state.withResultSetReturningError(func(resultSet *models.ResultSet) error {
		sb := new(strings.Builder)
		_ = applyToMarkedItems(resultSet, idx, func(idx int, item models.Item) error {
			if sb.Len() > 0 {
				fmt.Fprintln(sb, "---")
			}
			c.itemRendererService.RenderItem(sb, resultSet.Items()[idx], resultSet, true)
			itemCount += 1
			return nil
		})

		if err := c.pasteboardProvider.WriteText([]byte(sb.String())); err != nil {
			return err
		}
		return nil
	}); err != nil {
		return events.Error(err)
	}

	return events.StatusMsg(applyToN("", itemCount, "item", "items", " copied to clipboard"))
}
