package accountmigration

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/go-chi/chi/v5"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/middleware"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/oauth"
	"tangled.org/core/appview/pages"
	"tangled.org/core/rbac"
	"tangled.org/core/sets"
)

type AccountMigration struct {
	logger   *slog.Logger
	oauth    *oauth.OAuth
	pages    *pages.Pages
	db       *db.DB
	enforcer *rbac.Enforcer
}

func New(
	logger *slog.Logger,
	o *oauth.OAuth,
	p *pages.Pages,
	d *db.DB,
	enforcer *rbac.Enforcer,
) *AccountMigration {
	return &AccountMigration{
		logger:   logger,
		oauth:    o,
		pages:    p,
		db:       d,
		enforcer: enforcer,
	}
}

func (s *AccountMigration) Router() http.Handler {
	r := chi.NewRouter()
	r.Use(middleware.AuthMiddleware(s.oauth))

	r.Get("/", s.migratePage)
	r.Post("/listGitHubRepos", s.listGitHubRepos)
	r.Post("/start", s.startMigration)
	r.Get("/progress/rows", s.progressRows)
	return r
}

func (s *AccountMigration) migratePage(w http.ResponseWriter, r *http.Request) {
	user := s.oauth.GetMultiAccountUser(r)

	service := r.URL.Query().Get("service")
	switch service {
	case "github":
		knots, err := s.enforcer.GetKnotsForUser(user.Did)
		if err != nil {
			s.logger.Error("knots lookup failed", "did", user.Did, "err", err)
			knots = nil
		}
		if err := s.pages.AccountMigrateFromGitHub(w, pages.AccountMigrateFromGitHubParams{
			BaseParams: pages.BaseParamsFromContext(r.Context()),
			Knots:      knots,
			Repos: []pages.RepoImportParams{
				{
					SourceKind:  pages.RepoImportSourceGitHub,
					CloneUrl:    "https://github.com/boltlessengineer/rest.nvim",
					Name:        "rest.nvim",
					Description: "A very fast, powerful, extensible and asynchronous Neovim HTTP client written in Lua.",
					Website:     "https://tangled.org",
					Topics:      []string{"lua", "neovim", "curl", "http-client", "nvim", "neovim-plugin", "rest-client"},
					Selected:    true,
				},
				{
					SourceKind:  pages.RepoImportSourceGitHub,
					CloneUrl:    "https://github.com/boltlessengineer/dot",
					Name:        "super-long-repo-name-fddasfsafdsfasdfaasdfasdfasfasdfasfdasdfasdfsadfsadfasdfaasdfasfasdfdsafd00000s",
					Description: "dotfiles",
					Website:     "https://tangled.org",
					Topics:      []string{},
					Selected:    true,
				},
			},
		}); err != nil {
			s.logger.Error("failed to render", "err", err)
		}
	default:
		s.pages.AccountMigrate(w, pages.AccountMigrateParams{
			BaseParams: pages.BaseParamsFromContext(r.Context()),
		})
	}
}

type githubUserRepo struct {
	Name          string    `json:"name"`
	CloneUrl      string    `json:"clone_url"`
	Stars         int       `json:"stargazers_count"`
	PushedAt      time.Time `json:"pushed_at"`
	DefaultBranch string    `json:"default_branch"`
	Description   string    `json:"description"`
	Homepage      string    `json:"homepage"`
	Topics        []string  `json:"topics"`
	Fork          bool      `json:"fork"`
	Private       bool      `json:"private"`
	Archived      bool      `json:"archived"`
	Disabled      bool      `json:"disabled"`
}

func (s *AccountMigration) listGitHubRepos(w http.ResponseWriter, r *http.Request) {
	user := s.oauth.GetMultiAccountUser(r)

	username := strings.TrimSpace(r.FormValue("username"))
	if username == "" {
		s.pages.Notice(w, "migrate-error", "GitHub username is required.")
		return
	}

	query := url.Values{}
	query.Set("sort", "updated")
	query.Set("per_page", "80")

	endpoint := fmt.Sprintf("https://api.github.com/users/%s/repos?%s", url.PathEscape(username), query.Encode())
	req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, endpoint, nil)
	if err != nil {
		s.pages.Notice(w, "migrate-error", "Failed to build GitHub request.")
		return
	}
	req.Header.Set("Accept", "application/vnd.github+json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		s.logger.Error("github list repos failed", "username", username, "err", err)
		s.pages.Notice(w, "migrate-error", "Failed to reach GitHub. Try again.")
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		s.pages.Notice(w, "migrate-error", fmt.Sprintf("GitHub returned %d. Check the username.", resp.StatusCode))
		return
	}

	var githubRepos []githubUserRepo
	if err := json.NewDecoder(resp.Body).Decode(&githubRepos); err != nil {
		s.logger.Error("decode github response failed", "err", err)
		s.pages.Notice(w, "migrate-error", "Failed to parse GitHub response.")
		return
	}

	enqueued, err := db.ListEnqueuedGitRepoNames(r.Context(), s.db, user.Did)
	if err != nil {
		s.logger.Error("list enqueued names failed", "err", err)
		enqueued = map[string]struct{}{}
	}

	knots, err := s.enforcer.GetKnotsForUser(user.Did)
	if err != nil {
		s.logger.Error("knots lookup failed", "did", user.Did, "err", err)
		knots = nil
	}

	importRepos := make([]pages.RepoImportParams, 0, len(githubRepos))
	for _, ghrepo := range githubRepos {
		if ghrepo.Fork || ghrepo.Private || ghrepo.Archived || ghrepo.Disabled {
			continue
		}
		name := strings.ToLower(ghrepo.Name)
		_, migrated := enqueued[name]
		importRepos = append(importRepos, pages.RepoImportParams{
			SourceKind:  pages.RepoImportSourceGitHub,
			CloneUrl:    ghrepo.CloneUrl,
			Name:        name,
			Description: ghrepo.Description,
			Website:     ghrepo.Homepage,
			Topics:      ghrepo.Topics,
			Stars:       ghrepo.Stars,
			PushedAt:    ghrepo.PushedAt,
			Selected:    !migrated,
		})
	}

	// sort by stars desc, then most recently pushed
	slices.SortFunc(importRepos, func(a, b pages.RepoImportParams) int {
		if a.Stars != b.Stars {
			return b.Stars - a.Stars
		}
		return b.PushedAt.Compare(a.PushedAt)
	})

	if err := s.pages.AccountMigrateRepoListFragment(w, pages.AccountMigrateRepoListParams{
		Repos: importRepos,
		Knots: knots,
	}); err != nil {
		s.logger.Error("render repo-list fragment failed", "err", err)
	}
}

func (s *AccountMigration) startMigration(w http.ResponseWriter, r *http.Request) {
	user := s.oauth.GetMultiAccountUser(r)

	if err := r.ParseForm(); err != nil {
		s.pages.Notice(w, "migrate-error", "Invalid form submission.")
		return
	}

	knots, err := s.enforcer.GetKnotsForUser(user.Did)
	if err != nil {
		s.logger.Error("knots lookup failed", "did", user.Did, "err", err)
		s.pages.Notice(w, "migrate-error", "Failed to look up your knots.")
		return
	}
	allowed := sets.Collect(slices.Values(knots))

	sessionId := s.oauth.GetSessIdFromCookie(r)
	if sessionId == "" {
		s.pages.Notice(w, "migrate-error", "Session expired. Log in again.")
		return
	}

	count, err := strconv.Atoi(r.FormValue("count"))
	if err != nil || count <= 0 {
		s.pages.Notice(w, "migrate-error", "Invalid form submission.")
		return
	}

	s.logger.Debug("migrating repos", "count", count)

	rows := make([]db.GitRepoMigration, 0, count)
	seen := sets.New[string]()
	for i := range count {
		if r.FormValue(fmt.Sprintf("selected_%d", i)) == "" {
			s.logger.Warn("can't find selected repo", "i", i)
			continue
		}
		s.logger.Info("found selected repo", "i", i)

		cloneUrl := strings.TrimSpace(r.FormValue(fmt.Sprintf("clone_url_%d", i)))
		knot := strings.TrimSpace(r.FormValue(fmt.Sprintf("knot_%d", i)))
		name := strings.ToLower(strings.TrimSpace(r.FormValue(fmt.Sprintf("name_%d", i))))
		desc := strings.TrimSpace(r.FormValue(fmt.Sprintf("description_%d", i)))
		website := strings.TrimSpace(r.FormValue(fmt.Sprintf("website_%d", i)))
		topics := r.FormValue(fmt.Sprintf("topics_%d", i))

		if cloneUrl == "" || knot == "" || name == "" {
			s.pages.Notice(w, "migrate-error", "Each selected row needs a name, clone URL, and knot.")
			return
		}
		if err := models.ValidateRepoName(name); err != nil {
			s.pages.Notice(w, "migrate-error", fmt.Sprintf("Row %d: %s", i+1, err.Error()))
			return
		}
		if len([]rune(desc)) > 140 {
			s.pages.Notice(w, "migrate-error", fmt.Sprintf("Row %d: description must be 140 characters or fewer.", i+1))
			return
		}
		if !allowed.Contains(knot) {
			s.pages.Notice(w, "migrate-error", fmt.Sprintf("You are not a member of knot %q.", knot))
			return
		}
		if seen.Contains(name) {
			s.pages.Notice(w, "migrate-error", fmt.Sprintf("Duplicate repository name %q in selection.", name))
			return
		}
		seen.Insert(name)

		rows = append(rows, db.GitRepoMigration{
			OwnerDid:    syntax.DID(user.Did),
			SourceKind:  db.GitRepoMigrationSourceGitHub,
			CloneUrl:    cloneUrl,
			Name:        name,
			Knot:        knot,
			Description: desc,
			Website:     website,
			SessionID:   sessionId,
			Topics: func(s string) []string {
				if s == "" {
					return nil
				}
				parts := strings.Split(s, ",")
				out := parts[:0]
				for _, p := range parts {
					p = strings.TrimSpace(p)
					if p != "" {
						out = append(out, p)
					}
				}
				return out
			}(topics),
		})
	}

	if len(rows) == 0 {
		s.pages.Notice(w, "migrate-error", "Pick at least one repo.")
		return
	}

	s.logger.Info("inserting migrations", "len", len(rows))

	if err := db.InsertGitRepoMigrations(r.Context(), s.db, rows); err != nil {
		s.logger.Error("insert migrations failed", "err", err)
		s.pages.Notice(w, "migrate-error", "Failed to enqueue migrations.")
		return
	}

	w.Header().Set("HX-Redirect", "/settings/migration")
	http.Redirect(w, r, "/settings/migration", http.StatusSeeOther)
}

func (s *AccountMigration) progressRows(w http.ResponseWriter, r *http.Request) {
	user := s.oauth.GetMultiAccountUser(r)

	migrations, err := db.ListGitRepoMigrationsForOwner(r.Context(), s.db, user.Did)
	if err != nil {
		s.logger.Error("list migrations failed", "did", user.Did, "err", err)
	}

	if err := s.pages.AccountMigrateProgressRowsFragment(w, pages.AccountMigrateProgressParams{
		BaseParams: pages.BaseParamsFromContext(r.Context()),
		Migrations: migrations,
	}); err != nil {
		s.logger.Error("render progress rows failed", "err", err)
	}
}
