package rest

import (
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/pingcap/agent-git-service/internal/db"
	"github.com/pingcap/agent-git-service/internal/rest/respond"
	"github.com/pingcap/agent-git-service/internal/service"
)

type repositorySyncScheduleRequest struct {
	Mode string `json:"mode"`
	Cron string `json:"cron"`
}

// StartRepositorySyncGitHubInstall creates a one-time browser authorization
// request and returns GitHub's first-party App installation URL.
func (d *Deps) StartRepositorySyncGitHubInstall(w http.ResponseWriter, r *http.Request) {
	var req struct {
		ReturnTo string `json:"return_to"`
	}
	if err := decodeBodyStrict(r, &req); err != nil {
		respond.ValidationFailed(w, "invalid JSON")
		return
	}
	result, err := d.Svc.StartRepositorySyncGitHubInstall(r.Context(), req.ReturnTo)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusCreated, map[string]any{"install_url": result.InstallURL, "expires_at": result.ExpiresAt})
}

// StartRepositorySyncGitHubAuthorize connects the authenticated AGS user to a
// verified GitHub identity without installing the App again.
func (d *Deps) StartRepositorySyncGitHubAuthorize(w http.ResponseWriter, r *http.Request) {
	var req struct {
		ReturnTo string `json:"return_to"`
	}
	if err := decodeBodyStrict(r, &req); err != nil {
		respond.ValidationFailed(w, "invalid JSON")
		return
	}
	result, err := d.Svc.StartRepositorySyncGitHubAuthorize(r.Context(), req.ReturnTo)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusCreated, map[string]any{"authorize_url": result.InstallURL, "expires_at": result.ExpiresAt})
}

// CompleteRepositorySyncGitHubInstall is the public GitHub App callback. The
// state is one-time and tenant-scoped, so this endpoint must not require an AGS
// bearer token (normal browser navigation cannot attach one).
func (d *Deps) CompleteRepositorySyncGitHubInstall(w http.ResponseWriter, r *http.Request) {
	installationID, _ := strconv.ParseInt(r.URL.Query().Get("installation_id"), 10, 64)
	result, err := d.Svc.CompleteRepositorySyncGitHubInstall(r.Context(), service.CompleteRepositorySyncGitHubInstallInput{
		State: r.URL.Query().Get("state"), Code: r.URL.Query().Get("code"), InstallationID: installationID,
		SetupAction: r.URL.Query().Get("setup_action"), OAuthError: r.URL.Query().Get("error"),
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	http.Redirect(w, r, result.RedirectURL, http.StatusFound)
}

// ListRepositorySyncGitHubInstallations returns non-secret GitHub App picker
// data. Installation tokens and private keys never cross this boundary.
func (d *Deps) ListRepositorySyncGitHubInstallations(w http.ResponseWriter, r *http.Request) {
	account, err := d.Svc.GetRepositorySyncGitHubAccount(r.Context())
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	items, err := d.Svc.ListRepositorySyncGitHubInstallations(r.Context())
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	result := make([]map[string]any, 0, len(items))
	for _, item := range items {
		result = append(result, map[string]any{
			"connection_id": item.ConnectionID, "installation_id": item.InstallationID,
			"account_login": item.AccountLogin, "account_type": item.AccountType,
			"account_avatar_url": item.AccountAvatarURL, "management_url": item.ManagementURL,
			"repository_selection": item.RepositorySelection,
			"permissions":          item.Permissions, "permissions_valid": item.PermissionsValid,
			"missing_permissions": item.MissingPermissions,
		})
	}
	var accountJSON any
	if account != nil {
		accountJSON = map[string]any{
			"id": account.ID, "login": account.Login, "avatar_url": account.AvatarURL, "status": account.Status,
		}
	}
	respond.JSON(w, http.StatusOK, map[string]any{"account": accountJSON, "installations": result})
}

// DisconnectRepositorySyncGitHubAccount removes the authenticated viewer's
// personal GitHub setup authorization without uninstalling the GitHub App or
// altering existing repository-sync bindings.
func (d *Deps) DisconnectRepositorySyncGitHubAccount(w http.ResponseWriter, r *http.Request) {
	if err := d.Svc.DisconnectRepositorySyncGitHubAccount(r.Context()); err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (d *Deps) ListRepositorySyncGitHubRepositories(w http.ResponseWriter, r *http.Request) {
	connectionID := r.URL.Query().Get("connection_id")
	items, err := d.Svc.ListRepositorySyncGitHubRepositories(r.Context(), connectionID)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	result := make([]map[string]any, 0, len(items))
	for _, item := range items {
		result = append(result, map[string]any{
			"id": item.ID, "full_name": item.FullName, "private": item.Private,
			"visibility": item.Visibility, "archived": item.Archived,
			"connection_id": item.ConnectionID, "account_login": item.AccountLogin,
			"account_type": item.AccountType,
		})
	}
	respond.JSON(w, http.StatusOK, map[string]any{"repositories": result})
}

// CreateRepositorySyncBinding handles POST /api/ext/v1/sync/bindings.
func (d *Deps) CreateRepositorySyncBinding(w http.ResponseWriter, r *http.Request) {
	var req struct {
		AGSRepositoryID           uint                          `json:"ags_repository_id"`
		GitHubRepositoryID        string                        `json:"github_repository_id"`
		GitHubRepositoryFullName  string                        `json:"github_repository_full_name"`
		GitHubConnectionID        string                        `json:"github_connection_id"`
		InitialAuthority          string                        `json:"initial_authority"`
		ConfirmVisibilityExposure bool                          `json:"confirm_visibility_exposure"`
		Schedule                  repositorySyncScheduleRequest `json:"schedule"`
	}
	if err := decodeBodyStrict(r, &req); err != nil {
		respond.ValidationFailed(w, "invalid JSON")
		return
	}
	binding, err := d.Svc.CreateRepositorySyncBinding(r.Context(), service.CreateRepositorySyncBindingInput{
		AGSRepositoryID: req.AGSRepositoryID, GitHubRepositoryID: req.GitHubRepositoryID,
		GitHubRepositoryFullName: req.GitHubRepositoryFullName, GitHubConnectionID: req.GitHubConnectionID,
		InitialAuthority: req.InitialAuthority, ScheduleMode: req.Schedule.Mode, ScheduleCron: req.Schedule.Cron,
		ConfirmVisibilityExposure: req.ConfirmVisibilityExposure,
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusCreated, repositorySyncBindingJSON(binding))
}

// ListRepositorySyncBindings handles GET /api/ext/v1/sync/bindings.
func (d *Deps) ListRepositorySyncBindings(w http.ResponseWriter, r *http.Request) {
	repositoryID, _ := strconv.ParseUint(r.URL.Query().Get("ags_repository_id"), 10, 64)
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	bindings, err := d.Svc.ListRepositorySyncBindings(r.Context(), service.ListRepositorySyncBindingsInput{
		AGSRepositoryID: uint(repositoryID), Status: r.URL.Query().Get("status"), Limit: limit,
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	result := make([]map[string]any, 0, len(bindings))
	for _, binding := range bindings {
		result = append(result, repositorySyncBindingJSON(binding))
	}
	respond.JSON(w, http.StatusOK, map[string]any{"bindings": result})
}

// GetRepositorySyncOverview returns the aggregate read model for the global
// repository sync management page.
func (d *Deps) GetRepositorySyncOverview(w http.ResponseWriter, r *http.Request) {
	overview, err := d.Svc.GetRepositorySyncOverview(r.Context())
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	connections := make([]map[string]any, 0, len(overview.Connections))
	for _, connection := range overview.Connections {
		item := map[string]any{
			"binding": repositorySyncBindingJSON(connection.Binding),
			"health":  connection.Health,
		}
		if connection.LatestRun != nil {
			item["latest_run"] = repositorySyncRunJSON(*connection.LatestRun)
		} else {
			item["latest_run"] = nil
		}
		if connection.ActiveRun != nil {
			item["active_run"] = repositorySyncRunJSON(*connection.ActiveRun)
		} else {
			item["active_run"] = nil
		}
		if connection.Progress != nil && connection.ActiveRun != nil {
			item["progress"] = repositorySyncRunProgressJSON(connection.ActiveRun.ID, *connection.Progress)
		} else {
			item["progress"] = nil
		}
		connections = append(connections, item)
	}
	respond.JSON(w, http.StatusOK, map[string]any{
		"summary": map[string]any{
			"connections":        overview.Summary.Connections,
			"running":            overview.Summary.Running,
			"waiting_for_github": overview.Summary.WaitingForGitHub,
			"needs_attention":    overview.Summary.NeedsAttention,
			"healthy":            overview.Summary.Healthy,
			"paused":             overview.Summary.Paused,
			"not_started":        overview.Summary.NotStarted,
			"closed":             overview.Summary.Closed,
		},
		"connections": connections,
	})
}

// GetRepositorySyncBinding handles GET /api/ext/v1/sync/bindings/{binding_id}.
func (d *Deps) GetRepositorySyncBinding(w http.ResponseWriter, r *http.Request) {
	id, ok := mustUintParam(w, r, "binding_id")
	if !ok {
		return
	}
	binding, err := d.Svc.GetRepositorySyncBinding(r.Context(), id)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, repositorySyncBindingJSON(binding))
}

// UpdateRepositorySyncBinding handles PATCH /api/ext/v1/sync/bindings/{binding_id}.
func (d *Deps) UpdateRepositorySyncBinding(w http.ResponseWriter, r *http.Request) {
	id, ok := mustUintParam(w, r, "binding_id")
	if !ok {
		return
	}
	var req struct {
		Status                    *string                        `json:"status"`
		SyncEnabled               *bool                          `json:"sync_enabled"`
		ConfirmVisibilityExposure *bool                          `json:"confirm_visibility_exposure"`
		RefConflictPolicy         *string                        `json:"ref_conflict_policy"`
		Schedule                  *repositorySyncScheduleRequest `json:"schedule"`
	}
	if err := decodeBodyStrict(r, &req); err != nil {
		respond.ValidationFailed(w, "invalid JSON")
		return
	}
	input := service.UpdateRepositorySyncBindingInput{Status: req.Status, SyncEnabled: req.SyncEnabled, ConfirmVisibilityExposure: req.ConfirmVisibilityExposure, RefConflictPolicy: req.RefConflictPolicy}
	if req.Schedule != nil {
		input.ScheduleMode = &req.Schedule.Mode
		input.ScheduleCron = &req.Schedule.Cron
	}
	binding, err := d.Svc.UpdateRepositorySyncBinding(r.Context(), id, input)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, repositorySyncBindingJSON(binding))
}

// CreateRepositorySyncRun handles POST /api/ext/v1/sync/bindings/{binding_id}/runs.
func (d *Deps) CreateRepositorySyncRun(w http.ResponseWriter, r *http.Request) {
	bindingID, ok := mustUintParam(w, r, "binding_id")
	if !ok {
		return
	}
	var req struct {
		Mode      string `json:"mode"`
		AuditMode string `json:"audit_mode"`
	}
	if err := decodeBodyStrictOptional(r, &req); err != nil {
		respond.ValidationFailed(w, "invalid JSON")
		return
	}
	run, err := d.Svc.CreateRepositorySyncRun(r.Context(), bindingID, service.CreateRepositorySyncRunInput{Mode: req.Mode, AuditMode: req.AuditMode})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusAccepted, repositorySyncRunJSON(run))
}

// ListRepositorySyncRuns handles GET /api/ext/v1/sync/bindings/{binding_id}/runs.
func (d *Deps) ListRepositorySyncRuns(w http.ResponseWriter, r *http.Request) {
	bindingID, ok := mustUintParam(w, r, "binding_id")
	if !ok {
		return
	}
	limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
	if err != nil {
		limit = 30
	}
	runs, err := d.Svc.ListRepositorySyncRuns(r.Context(), bindingID, limit)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	result := make([]map[string]any, 0, len(runs))
	for _, run := range runs {
		result = append(result, repositorySyncRunJSON(run))
	}
	respond.JSON(w, http.StatusOK, map[string]any{"runs": result})
}

// ListRepositorySyncActivity returns recent runs across all repository sync
// bindings the current viewer can administer.
func (d *Deps) ListRepositorySyncActivity(w http.ResponseWriter, r *http.Request) {
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	afterID, _ := strconv.ParseUint(r.URL.Query().Get("after_id"), 10, 64)
	page, err := d.Svc.ListRepositorySyncActivity(r.Context(), service.ListRepositorySyncActivityInput{
		Status: r.URL.Query().Get("status"), AfterID: uint(afterID), Limit: limit,
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	items := make([]map[string]any, 0, len(page.Items))
	for _, item := range page.Items {
		items = append(items, map[string]any{
			"binding": repositorySyncBindingJSON(item.Binding),
			"run":     repositorySyncRunJSON(item.Run),
		})
	}
	respond.JSON(w, http.StatusOK, map[string]any{
		"items": items, "next_after_id": page.NextAfterID, "has_more": page.HasMore,
	})
}

// GetRepositorySyncRun handles GET /api/ext/v1/sync/runs/{run_id}.
func (d *Deps) GetRepositorySyncRun(w http.ResponseWriter, r *http.Request) {
	id, ok := mustUintParam(w, r, "run_id")
	if !ok {
		return
	}
	run, err := d.Svc.GetRepositorySyncRun(r.Context(), id)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, repositorySyncRunJSON(run))
}

// GetRepositorySyncRunProgress handles GET /api/ext/v1/sync/runs/{run_id}/progress.
func (d *Deps) GetRepositorySyncRunProgress(w http.ResponseWriter, r *http.Request) {
	runID, ok := mustUintParam(w, r, "run_id")
	if !ok {
		return
	}
	progress, err := d.Svc.GetRepositorySyncRunProgress(r.Context(), runID)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, repositorySyncRunProgressJSON(runID, progress))
}

func repositorySyncRunProgressJSON(runID uint, progress service.RepositorySyncRunProgress) map[string]any {
	byObjectType := make(map[string]any, len(progress.ByObjectType))
	for objectType, counts := range progress.ByObjectType {
		byObjectType[objectType] = repositorySyncProgressCountsJSON(counts)
	}
	return map[string]any{
		"run_id":         runID,
		"total":          progress.Total,
		"processed":      progress.Processed,
		"remaining":      progress.Remaining,
		"planned":        progress.Planned,
		"applying":       progress.Applying,
		"verifying":      progress.Verifying,
		"completed":      progress.Completed,
		"blocked":        progress.Blocked,
		"failed":         progress.Failed,
		"superseded":     progress.Superseded,
		"by_object_type": byObjectType,
	}
}

func repositorySyncProgressCountsJSON(progress service.RepositorySyncRunProgressCounts) map[string]any {
	return map[string]any{
		"total": progress.Total, "processed": progress.Processed, "remaining": progress.Remaining,
		"planned": progress.Planned, "applying": progress.Applying, "verifying": progress.Verifying,
		"completed": progress.Completed, "blocked": progress.Blocked, "failed": progress.Failed,
		"superseded": progress.Superseded,
	}
}

// ListRepositorySyncRunItems handles GET /api/ext/v1/sync/runs/{run_id}/items.
func (d *Deps) ListRepositorySyncRunItems(w http.ResponseWriter, r *http.Request) {
	runID, ok := mustUintParam(w, r, "run_id")
	if !ok {
		return
	}
	limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
	afterID, _ := strconv.ParseUint(r.URL.Query().Get("after_id"), 10, 64)
	items, err := d.Svc.ListRepositorySyncRunItems(r.Context(), runID, service.ListRepositorySyncRunItemsInput{
		State: r.URL.Query().Get("state"), ObjectType: r.URL.Query().Get("object_type"),
		Action: r.URL.Query().Get("action"), AfterID: uint(afterID), Limit: limit,
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	result := make([]map[string]any, 0, len(items))
	var nextAfterID uint
	for _, item := range items {
		result = append(result, repositorySyncRunItemJSON(item))
		nextAfterID = item.ID
	}
	respond.JSON(w, http.StatusOK, map[string]any{"items": result, "next_after_id": nextAfterID})
}

// CancelRepositorySyncRun handles POST /api/ext/v1/sync/runs/{run_id}/cancel.
func (d *Deps) CancelRepositorySyncRun(w http.ResponseWriter, r *http.Request) {
	id, ok := mustUintParam(w, r, "run_id")
	if !ok {
		return
	}
	run, err := d.Svc.CancelRepositorySyncRun(r.Context(), id)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusOK, repositorySyncRunJSON(run))
}

// RecoverRepositorySyncRun handles POST /api/ext/v1/sync/runs/{run_id}/recover.
func (d *Deps) RecoverRepositorySyncRun(w http.ResponseWriter, r *http.Request) {
	id, ok := mustUintParam(w, r, "run_id")
	if !ok {
		return
	}
	run, err := d.Svc.RecoverRepositorySyncRun(r.Context(), id)
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusAccepted, repositorySyncRunJSON(run))
}

// CreateRepositorySyncHandoff handles POST /api/ext/v1/sync/bindings/{binding_id}/handoff.
func (d *Deps) CreateRepositorySyncHandoff(w http.ResponseWriter, r *http.Request) {
	bindingID, ok := mustUintParam(w, r, "binding_id")
	if !ok {
		return
	}
	var req struct {
		ToSide                    string `json:"to_side"`
		ExpectedAuthorityEpoch    uint64 `json:"expected_authority_epoch"`
		AuthorityWritesStopped    bool   `json:"authority_writes_stopped"`
		ConfirmVisibilityExposure bool   `json:"confirm_visibility_exposure"`
	}
	if err := decodeBodyStrict(r, &req); err != nil {
		respond.ValidationFailed(w, "invalid JSON")
		return
	}
	run, err := d.Svc.CreateRepositorySyncHandoff(r.Context(), bindingID, service.CreateRepositorySyncHandoffInput{
		ToProvider: req.ToSide, ExpectedAuthorityEpoch: req.ExpectedAuthorityEpoch,
		AuthorityWritesStopped:    req.AuthorityWritesStopped,
		ConfirmVisibilityExposure: req.ConfirmVisibilityExposure,
	})
	if err != nil {
		respond.ServiceErrorRequest(r, w, err)
		return
	}
	respond.JSON(w, http.StatusAccepted, repositorySyncRunJSON(run))
}

func repositorySyncBindingJSON(binding db.SyncBinding) map[string]any {
	endpoints := make([]map[string]any, 0, len(binding.Endpoints))
	authorityProvider := ""
	for _, endpoint := range binding.Endpoints {
		endpoints = append(endpoints, map[string]any{
			"side": endpoint.Side, "provider": endpoint.Provider,
			"repository_id": endpoint.RepositoryID, "repository_full_name": endpoint.RepositoryFullName,
			"connection_id": endpoint.ConnectionID,
		})
		if endpoint.Side == binding.AuthoritySide {
			authorityProvider = endpoint.Provider
		}
	}
	return map[string]any{
		"id": binding.ID, "status": binding.Status,
		"authority_side": binding.AuthoritySide, "authority_provider": authorityProvider,
		"authority_epoch":     binding.AuthorityEpoch,
		"ref_conflict_policy": binding.RefConflictPolicy,
		"sync_enabled":        binding.SyncEnabled, "schedule": map[string]any{"mode": binding.ScheduleMode, "cron": binding.ScheduleCron, "next_run_at": formatTimePtr(binding.NextRunAt)},
		"visibility_exposure_confirmed": binding.VisibilityConfirmation != "",
		"endpoints":                     endpoints, "last_run_id": binding.LastRunID,
		"last_converged_run_id": binding.LastConvergedRunID, "last_sync_at": formatTimePtr(binding.LastSyncAt),
		"last_full_audit_at": formatTimePtr(binding.LastFullAuditAt),
		"created_at":         binding.CreatedAt.Format(time.RFC3339), "updated_at": binding.UpdatedAt.Format(time.RFC3339),
	}
}

func repositorySyncRunJSON(run db.SyncRun) map[string]any {
	return map[string]any{
		"id": run.ID, "binding_id": run.BindingID, "kind": run.Kind, "mode": run.Mode,
		"authority_epoch": run.AuthorityEpoch, "source_side": run.SourceSide, "target_side": run.TargetSide,
		"handoff_to_side": run.HandoffToSide, "status": run.Status, "phase": run.Phase,
		"stage":        run.Stage,
		"planner_mode": run.PlannerMode, "mapper_version": run.MapperVersion,
		"audit_mode":              run.AuditMode,
		"source_inventory_digest": run.SourceInventoryDigest, "target_inventory_digest": run.TargetInventoryDigest,
		"counters": rawJSONOrEmptyObject(run.CountersJSON), "report": rawJSONOrEmptyObject(run.ReportJSON),
		"cancel_requested": run.CancelRequested, "error_code": run.ErrorCode, "error": string(run.ErrorSummary),
		"started_at": formatTimePtr(run.StartedAt), "completed_at": formatTimePtr(run.CompletedAt), "last_progress_at": formatTimePtr(run.LastProgressAt), "retry_after_at": formatTimePtr(run.RetryAfterAt),
		"created_at": run.CreatedAt.Format(time.RFC3339), "updated_at": run.UpdatedAt.Format(time.RFC3339),
	}
}

func repositorySyncRunItemJSON(item db.SyncRunItem) map[string]any {
	return map[string]any{
		"id": item.ID, "run_id": item.RunID, "object_link_id": item.ObjectLinkID,
		"phase": item.Phase, "object_type": item.ObjectType, "object_key": item.ObjectKey,
		"action": item.Action, "state": item.State, "dependency_phase": item.DependencyPhase,
		"payload_bytes": item.PayloadBytes, "source_revision": item.SourceRevision,
		"source_hash": item.SourceHash, "desired_hash": item.DesiredHash, "target_revision": item.ExpectedTargetRevision,
		"target_hash": item.ExpectedTargetHash, "attempt_count": item.AttemptCount,
		"error_code": item.ErrorCode, "error_message": string(item.ErrorMessage),
		"completed_at": formatTimePtr(item.CompletedAt), "created_at": item.CreatedAt.Format(time.RFC3339),
		"updated_at": item.UpdatedAt.Format(time.RFC3339),
	}
}

func rawJSONOrEmptyObject(value db.LargeText) any {
	if value == "" {
		return map[string]any{}
	}
	var decoded any
	if err := json.Unmarshal([]byte(value), &decoded); err != nil {
		return string(value)
	}
	return decoded
}
