package data

import (
	"context"
	_ "embed"
	"fmt"
	"time"

	"github.com/brianvoe/gofakeit/v7"

	"github.com/SecurityBrewery/catalyst/app/auth"
	"github.com/SecurityBrewery/catalyst/app/auth/password"
	"github.com/SecurityBrewery/catalyst/app/database"
	"github.com/SecurityBrewery/catalyst/app/database/sqlc"
	"github.com/SecurityBrewery/catalyst/app/pointer"
)

const (
	minimumUserCount   = 1
	minimumTicketCount = 1
)

var (
	//go:embed scripts/createticket.py
	createTicketPy string
	//go:embed scripts/alertingest.py
	alertIngestPy string
	//go:embed scripts/assigntickets.py
	assignTicketsPy string
)

func GenerateDemoData(ctx context.Context, queries *sqlc.Queries, userCount, ticketCount int) error {
	if userCount < minimumUserCount {
		userCount = minimumUserCount
	}

	if ticketCount < minimumTicketCount {
		ticketCount = minimumTicketCount
	}

	types, err := database.PaginateItems(ctx, func(ctx context.Context, offset, limit int64) ([]sqlc.ListTypesRow, error) {
		return queries.ListTypes(ctx, sqlc.ListTypesParams{Limit: limit, Offset: offset})
	})
	if err != nil {
		return fmt.Errorf("failed to list types: %w", err)
	}

	users, err := generateDemoUsers(ctx, queries, userCount, ticketCount)
	if err != nil {
		return fmt.Errorf("failed to create user records: %w", err)
	}

	if len(types) == 0 {
		return fmt.Errorf("no types found")
	}

	if len(users) == 0 {
		return fmt.Errorf("no users found")
	}

	if err := generateDemoTickets(ctx, queries, users, types, ticketCount); err != nil {
		return fmt.Errorf("failed to create ticket records: %w", err)
	}

	if err := generateDemoReactions(ctx, queries, ticketCount); err != nil {
		return fmt.Errorf("failed to create reaction records: %w", err)
	}

	if err := generateDemoGroups(ctx, queries, users, ticketCount); err != nil {
		return fmt.Errorf("failed to create group records: %w", err)
	}

	return nil
}

func generateDemoUsers(ctx context.Context, queries *sqlc.Queries, count, ticketCount int) ([]sqlc.User, error) {
	users := make([]sqlc.User, 0, count)

	// create the test user
	user, err := queries.GetUser(ctx, "u_test")
	if err != nil {
		newUser, err := createTestUser(ctx, queries)
		if err != nil {
			return nil, err
		}

		users = append(users, newUser)
	} else {
		users = append(users, user)
	}

	for range count - 1 {
		newUser, err := createDemoUser(ctx, queries, ticketCount)
		if err != nil {
			return nil, err
		}

		users = append(users, newUser)
	}

	return users, nil
}

func createDemoUser(ctx context.Context, queries *sqlc.Queries, ticketCount int) (sqlc.User, error) {
	username := gofakeit.Username()

	passwordHash, tokenKey, err := password.Hash(gofakeit.Password(true, true, true, true, false, 16))
	if err != nil {
		return sqlc.User{}, fmt.Errorf("failed to hash password: %w", err)
	}

	created, updated := dates(ticketCount)

	return queries.InsertUser(ctx, sqlc.InsertUserParams{
		ID:           database.GenerateID("u"),
		Name:         pointer.Pointer(gofakeit.Name()),
		Email:        pointer.Pointer(username + "@catalyst-soar.com"),
		Username:     username,
		PasswordHash: passwordHash,
		TokenKey:     tokenKey,
		Active:       gofakeit.Bool(),
		Created:      created,
		Updated:      updated,
	})
}

var ticketCreated = time.Date(2025, 2, 1, 11, 29, 35, 0, time.UTC)

func generateDemoTickets(ctx context.Context, queries *sqlc.Queries, users []sqlc.User, types []sqlc.ListTypesRow, count int) error { //nolint:cyclop
	for range count {
		newTicket, err := createDemoTicket(ctx, queries, random(types), random(users).ID, fakeTicketTitle(), fakeTicketDescription(), count)
		if err != nil {
			return fmt.Errorf("failed to create ticket: %w", err)
		}

		for range gofakeit.IntRange(1, 5) {
			_, err := createDemoComment(ctx, queries, newTicket.ID, random(users).ID, fakeTicketComment(), count)
			if err != nil {
				return fmt.Errorf("failed to create comment for ticket %s: %w", newTicket.ID, err)
			}
		}

		for range gofakeit.IntRange(1, 5) {
			_, err := createDemoTimeline(ctx, queries, newTicket.ID, fakeTicketTimelineMessage(), count)
			if err != nil {
				return fmt.Errorf("failed to create timeline for ticket %s: %w", newTicket.ID, err)
			}
		}

		for range gofakeit.IntRange(1, 5) {
			_, err := createDemoTask(ctx, queries, newTicket.ID, random(users).ID, fakeTicketTask(), count)
			if err != nil {
				return fmt.Errorf("failed to create task for ticket %s: %w", newTicket.ID, err)
			}
		}

		for range gofakeit.IntRange(1, 5) {
			_, err := createDemoLink(ctx, queries, newTicket.ID, random([]string{"Blog", "Forum", "Wiki", "Documentation"}), gofakeit.URL(), count)
			if err != nil {
				return fmt.Errorf("failed to create link for ticket %s: %w", newTicket.ID, err)
			}
		}
	}

	return nil
}

func createDemoTicket(ctx context.Context, queries *sqlc.Queries, ticketType sqlc.ListTypesRow, userID, name, description string, ticketCount int) (sqlc.Ticket, error) {
	created, updated := dates(ticketCount)

	ticket, err := queries.InsertTicket(
		ctx,
		sqlc.InsertTicketParams{
			ID:          database.GenerateID(ticketType.Singular),
			Name:        name,
			Description: description,
			Open:        gofakeit.Bool(),
			Owner:       &userID,
			Schema:      marshal(map[string]any{"type": "object", "properties": map[string]any{"tlp": map[string]any{"title": "TLP", "type": "string"}}}),
			State:       marshal(map[string]any{"severity": "Medium"}),
			Type:        ticketType.ID,
			Created:     created,
			Updated:     updated,
		},
	)
	if err != nil {
		return sqlc.Ticket{}, fmt.Errorf("failed to create ticket for user %s: %w", userID, err)
	}

	return ticket, nil
}

func createDemoComment(ctx context.Context, queries *sqlc.Queries, ticketID, userID, message string, ticketCount int) (*sqlc.Comment, error) {
	created, updated := dates(ticketCount)

	comment, err := queries.InsertComment(ctx, sqlc.InsertCommentParams{
		ID:      database.GenerateID("c"),
		Ticket:  ticketID,
		Author:  userID,
		Message: message,
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create comment for ticket %s: %w", ticketID, err)
	}

	return &comment, nil
}

func createDemoTimeline(ctx context.Context, queries *sqlc.Queries, ticketID, message string, ticketCount int) (*sqlc.Timeline, error) {
	created, updated := dates(ticketCount)

	timeline, err := queries.InsertTimeline(ctx, sqlc.InsertTimelineParams{
		ID:      database.GenerateID("tl"),
		Ticket:  ticketID,
		Message: message,
		Time:    ticketCreated,
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create timeline for ticket %s: %w", ticketID, err)
	}

	return &timeline, nil
}

func createDemoTask(ctx context.Context, queries *sqlc.Queries, ticketID, userID, name string, ticketCount int) (*sqlc.Task, error) {
	created, updated := dates(ticketCount)

	task, err := queries.InsertTask(ctx, sqlc.InsertTaskParams{
		ID:      database.GenerateID("t"),
		Ticket:  ticketID,
		Owner:   &userID,
		Name:    name,
		Open:    gofakeit.Bool(),
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create task for ticket %s: %w", ticketID, err)
	}

	return &task, nil
}

func createDemoLink(ctx context.Context, queries *sqlc.Queries, ticketID, name, url string, ticketCount int) (*sqlc.Link, error) {
	created, updated := dates(ticketCount)

	link, err := queries.InsertLink(ctx, sqlc.InsertLinkParams{
		ID:      database.GenerateID("l"),
		Ticket:  ticketID,
		Name:    name,
		Url:     url,
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create link for ticket %s: %w", ticketID, err)
	}

	return &link, nil
}

func generateDemoReactions(ctx context.Context, queries *sqlc.Queries, ticketCount int) error {
	created, updated := dates(ticketCount)

	_, err := queries.InsertReaction(ctx, sqlc.InsertReactionParams{
		ID:          "r-schedule",
		Name:        "Create New Ticket",
		Trigger:     "schedule",
		Triggerdata: marshal(map[string]any{"expression": "12 * * * *"}),
		Action:      "python",
		Actiondata: marshal(map[string]any{
			"requirements": "requests",
			"script":       createTicketPy,
		}),
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create reaction for schedule trigger: %w", err)
	}

	created, updated = dates(ticketCount)

	_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
		ID:          "r-webhook",
		Name:        "Alert Ingest Webhook",
		Trigger:     "webhook",
		Triggerdata: marshal(map[string]any{"token": "1234567890", "path": "webhook"}),
		Action:      "python",
		Actiondata: marshal(map[string]any{
			"requirements": "requests",
			"script":       alertIngestPy,
		}),
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create reaction for webhook trigger: %w", err)
	}

	created, updated = dates(ticketCount)

	_, err = queries.InsertReaction(ctx, sqlc.InsertReactionParams{
		ID:          "r-hook",
		Name:        "Assign new Tickets",
		Trigger:     "hook",
		Triggerdata: marshal(map[string]any{"collections": []any{"tickets"}, "events": []any{"create"}}),
		Action:      "python",
		Actiondata: marshal(map[string]any{
			"requirements": "requests",
			"script":       assignTicketsPy,
		}),
		Created: created,
		Updated: updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create reaction for hook trigger: %w", err)
	}

	return nil
}

func generateDemoGroups(ctx context.Context, queries *sqlc.Queries, users []sqlc.User, ticketCount int) error { //nolint:cyclop
	created, updated := dates(ticketCount)

	_, err := queries.InsertGroup(ctx, sqlc.InsertGroupParams{
		ID:          "team-ir",
		Name:        "IR Team",
		Permissions: auth.ToJSONArray(ctx, []string{}),
		Created:     created,
		Updated:     updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create IR team group: %w", err)
	}

	created, updated = dates(ticketCount)

	_, err = queries.InsertGroup(ctx, sqlc.InsertGroupParams{
		ID:          "team-seceng",
		Name:        "Security Engineering Team",
		Permissions: auth.ToJSONArray(ctx, []string{}),
		Created:     created,
		Updated:     updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create IR team group: %w", err)
	}

	created, updated = dates(ticketCount)

	_, err = queries.InsertGroup(ctx, sqlc.InsertGroupParams{
		ID:          "team-security",
		Name:        "Security Team",
		Permissions: auth.ToJSONArray(ctx, []string{}),
		Created:     created,
		Updated:     updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create security team group: %w", err)
	}

	created, updated = dates(ticketCount)

	_, err = queries.InsertGroup(ctx, sqlc.InsertGroupParams{
		ID:          "g-engineer",
		Name:        "Engineer",
		Permissions: auth.ToJSONArray(ctx, []string{"reaction:read", "reaction:write"}),
		Created:     created,
		Updated:     updated,
	})
	if err != nil {
		return fmt.Errorf("failed to create analyst group: %w", err)
	}

	for _, user := range users {
		group := gofakeit.RandomString([]string{"team-seceng", "team-ir"})
		if user.ID == "u_test" {
			group = "admin"
		}

		if err := queries.AssignGroupToUser(ctx, sqlc.AssignGroupToUserParams{
			UserID:  user.ID,
			GroupID: group,
		}); err != nil {
			return fmt.Errorf("failed to assign group %s to user %s: %w", group, user.ID, err)
		}
	}

	err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
		ParentGroupID: "team-ir",
		ChildGroupID:  "analyst",
	})
	if err != nil {
		return fmt.Errorf("failed to assign parent group: %w", err)
	}

	err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
		ParentGroupID: "team-seceng",
		ChildGroupID:  "g-engineer",
	})
	if err != nil {
		return fmt.Errorf("failed to assign parent group: %w", err)
	}

	err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
		ParentGroupID: "team-ir",
		ChildGroupID:  "team-security",
	})
	if err != nil {
		return fmt.Errorf("failed to assign parent group: %w", err)
	}

	err = queries.AssignParentGroup(ctx, sqlc.AssignParentGroupParams{
		ParentGroupID: "team-seceng",
		ChildGroupID:  "team-security",
	})
	if err != nil {
		return fmt.Errorf("failed to assign parent group: %w", err)
	}

	return nil
}

func weeksAgo(c int) time.Time {
	return time.Now().UTC().AddDate(0, 0, -7*c)
}

func dates(ticketCount int) (time.Time, time.Time) {
	const ticketsPerWeek = 10

	weeks := ticketCount / ticketsPerWeek

	created := gofakeit.DateRange(weeksAgo(1), weeksAgo(weeks+1)).UTC()
	updated := gofakeit.DateRange(created, time.Now()).UTC()

	return created, updated
}
