package service

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/i18n"
	"github.com/1Panel-dev/1Panel/agent/utils/appicon"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
	"github.com/1Panel-dev/1Panel/agent/utils/xpack"
)

type appSyncContext struct {
	task           *task.Task
	httpClient     http.Client
	baseRemoteUrl  string
	systemVersion  string
	appsMap        map[string]model.App
	settingService ISettingService
	list           *dto.AppList
	oldAppIds      []uint
	appTags        []*model.AppTag
	skipMetaSync   bool
	pendingIcons   map[string]string
}

func (a AppService) createSyncAppStoreTask(sharedCtx **appSyncContext) func(t *task.Task) error {
	return func(t *task.Task) (err error) {
		t.LogStart(i18n.GetMsgByKey("AppStore") + " " + i18n.GetMsgByKey("TaskSync"))

		updateRes, err := a.GetAppUpdate()
		if err != nil {
			t.LogFailedWithErr(i18n.GetMsgByKey("CheckAppStoreUpdate"), err)
			return err
		}
		if !updateRes.CanUpdate {
			if updateRes.IsSyncing {
				t.Log(i18n.GetMsgByKey("AppStoreIsSyncing"))
				return nil
			}
			global.LOG.Infof("[AppStore] Appstore is up to date")
			t.Log(i18n.GetMsgByKey("AppStoreIsUpToDate"))
			*sharedCtx = &appSyncContext{skipMetaSync: true}
			t.LogSuccess(i18n.GetMsgByKey("AppStore") + " " + i18n.GetMsgByKey("TaskSync"))
			return nil
		}

		list := &dto.AppList{}
		if updateRes.AppList == nil {
			list, err = getAppList()
			if err != nil {
				t.LogFailedWithErr(i18n.GetMsgByKey("DownloadAppList"), err)
				return err
			}
		} else {
			list = updateRes.AppList
		}

		settingService := NewISettingService()
		if err := settingService.Update("AppStoreSyncStatus", constant.StatusSyncing); err != nil {
			global.LOG.Warnf("[AppStore] failed to update sync status to syncing: %v", err)
		}

		setting, err := settingService.GetSettingInfo()
		if err != nil {
			t.LogFailedWithErr("GetSettingInfo", err)
			return err
		}

		ctx := &appSyncContext{
			task:           t,
			httpClient:     http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.MultiNodeProvider.LoadRequestTransport()},
			baseRemoteUrl:  fmt.Sprintf("%s/%s/1panel", global.AppRepoURL(), global.CONF.Base.Mode),
			systemVersion:  setting.SystemVersion,
			settingService: settingService,
			list:           list,
			appTags:        make([]*model.AppTag, 0),
			pendingIcons:   make(map[string]string),
		}

		if err = SyncTags(list.Extra); err != nil {
			t.LogFailedWithErr(i18n.GetMsgByKey("SyncTags"), err)
			return err
		}
		deleteCustomApp()

		oldApps, err := appRepo.GetBy(appRepo.WithNotLocal())
		if err != nil {
			t.LogFailedWithErr(i18n.GetMsgByKey("LoadLocalApps"), err)
			return err
		}
		ctx.oldAppIds = make([]uint, 0, len(oldApps))
		for _, old := range oldApps {
			ctx.oldAppIds = append(ctx.oldAppIds, old.ID)
		}

		ctx.appsMap, ctx.pendingIcons = getApps(oldApps, list.Apps, setting.SystemVersion, t)

		var addCount, updateCount, deleteCount int
		if err = ctx.classifyAndPersistAppsWithStats(&addCount, &updateCount, &deleteCount); err != nil {
			t.LogFailedWithErr(i18n.GetMsgByKey("PersistApps"), err)
			return err
		}

		if err := settingService.Update("AppStoreSyncStatus", constant.StatusSyncSuccess); err != nil {
			global.LOG.Warnf("[AppStore] failed to update sync status to success: %v", err)
		}
		if err := settingService.Update("AppStoreLastModified", strconv.Itoa(list.LastModified)); err != nil {
			global.LOG.Warnf("[AppStore] failed to update last modified: %v", err)
		}
		global.LOG.Infof("[AppStore] Appstore sync completed")

		*sharedCtx = ctx
		t.LogSuccess(i18n.GetMsgByKey("AppStore") + " " + i18n.GetMsgByKey("TaskSync"))
		return nil
	}
}

type appWorkItem struct {
	appDef  dto.AppDefine
	app     model.App
	iconUrl string
	hadIcon bool
}

type appWorkResult struct {
	appKey     string
	app        model.App
	iconStatus int
	hadIcon    bool
	httpFailed bool
}

func (c *appSyncContext) processOneApp(item appWorkItem) appWorkResult {
	app := item.app
	l := item.appDef
	result := appWorkResult{
		appKey:  l.AppProperty.Key,
		hadIcon: item.hadIcon,
	}

	if item.hadIcon {
		status, iconField := c.downloadAppIcon(item.iconUrl, l.AppProperty.Key, app.Icon)
		result.iconStatus = status
		switch status {
		case http.StatusOK:
			app.Icon = iconField
		case http.StatusNotModified:
		default:
			result.httpFailed = true
		}
	}

	app.TagsKey = l.AppProperty.Tags
	if l.AppProperty.Recommend > 0 {
		app.Recommend = l.AppProperty.Recommend
	} else {
		app.Recommend = 9999
	}
	app.ReadMe = l.ReadMe
	app.LastModified = l.LastModified

	versions := l.Versions
	detailsMap := getAppDetails(app.Details, versions)
	for _, v := range versions {
		version := v.Name
		detail := detailsMap[version]
		versionUrl := fmt.Sprintf("%s/%s/%s", c.baseRemoteUrl, app.Key, version)

		paramByte, _ := json.Marshal(v.AppForm)
		var appForm dto.AppForm
		_ = json.Unmarshal(paramByte, &appForm)

		if appForm.SupportVersion > 0 && common.CompareVersion(strconv.FormatFloat(appForm.SupportVersion, 'f', -1, 64), c.systemVersion) {
			delete(detailsMap, version)
			continue
		}

		if _, ok := InitTypes[app.Type]; ok {
			dockerComposeUrl := fmt.Sprintf("%s/%s", versionUrl, "docker-compose.yml")
			_, composeRes, err := req_helper.HandleRequestWithClient(&c.httpClient, dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
			if err == nil {
				detail.DockerCompose = string(composeRes)
			} else {
				result.httpFailed = true
			}
		} else {
			detail.DockerCompose = ""
		}

		detail.Params = string(paramByte)
		detail.DownloadUrl = fmt.Sprintf("%s/%s", versionUrl, app.Key+"-"+version+".tar.gz")
		detail.DownloadCallBackUrl = v.DownloadCallBackUrl
		detail.Update = true
		detail.LastModified = v.LastModified
		detailsMap[version] = detail
	}

	newDetails := make([]model.AppDetail, 0, len(detailsMap))
	for _, detail := range detailsMap {
		newDetails = append(newDetails, detail)
	}
	app.Details = newDetails

	result.app = app
	return result
}

func (c *appSyncContext) syncAppIconsAndDetails() error {
	total := len(c.list.Apps)
	global.LOG.Infof("[AppStore] sync app detail start, total apps: %d", total)

	workItems := make([]appWorkItem, 0, total)
	for _, l := range c.list.Apps {
		app, ok := c.appsMap[l.AppProperty.Key]
		if !ok {
			continue
		}
		iconUrl, hasPending := c.pendingIcons[l.AppProperty.Key]
		workItems = append(workItems, appWorkItem{
			appDef:  l,
			app:     app,
			iconUrl: iconUrl,
			hadIcon: hasPending,
		})
	}

	totalWork := len(workItems)
	if totalWork == 0 {
		return nil
	}

	const maxWorkers = 4
	var (
		failFlag atomic.Bool
		workCh   = make(chan int, maxWorkers)
		resultCh = make(chan appWorkResult, maxWorkers)
		wg       sync.WaitGroup
	)

	wg.Add(maxWorkers)
	for range maxWorkers {
		go func() {
			defer wg.Done()
			for idx := range workCh {
				result := c.processOneApp(workItems[idx])
				resultCh <- result
				if result.httpFailed {
					failFlag.Store(true)
				}
			}
		}()
	}
	go func() { wg.Wait(); close(resultCh) }()

	var fed atomic.Int32
	go func() {
		for i := range workItems {
			if failFlag.Load() {
				break
			}
			workCh <- i
			fed.Store(int32(i + 1))
		}
		close(workCh)
	}()

	var (
		completed     int
		icon200Count  int
		icon304Count  int
		iconFailCount int
	)
	milestones := [4]int{totalWork / 4, totalWork / 2, totalWork * 3 / 4, totalWork}
	nextMS := 0
	checkProgress := func() {
		if nextMS < len(milestones) && completed >= milestones[nextMS] {
			c.task.LogWithProgress(i18n.GetMsgByKey("SyncAppDetail"), completed, totalWork)
			nextMS++
		}
	}

	applyResult := func(result appWorkResult) {
		c.appsMap[result.appKey] = result.app
		if result.hadIcon {
			switch result.iconStatus {
			case http.StatusOK:
				icon200Count++
			case http.StatusNotModified:
				icon304Count++
			default:
				iconFailCount++
			}
		}
		completed++
		checkProgress()
	}

	for result := range resultCh {
		applyResult(result)
	}

	fedCount := int(fed.Load())
	if failFlag.Load() && fedCount < len(workItems) {
		global.LOG.Warnf("[AppStore] HTTP failure detected, falling back to serial for remaining %d apps", len(workItems)-fedCount)
		for i := fedCount; i < len(workItems); i++ {
			result := c.processOneApp(workItems[i])
			applyResult(result)
		}
	}

	global.LOG.Infof("[AppStore] icon download completed - total: %d, success(200): %d, cached(304): %d, failed: %d",
		total, icon200Count, icon304Count, iconFailCount)

	return nil
}

func (c *appSyncContext) downloadAppIcon(iconUrl, appKey, oldIcon string) (status int, iconField string) {
	iconFileName, existingEtag := appicon.ParseIconField(oldIcon)

	reqHeaders := make(map[string]string)
	if existingEtag != "" && iconFileName != "" && appicon.IconFileExists(iconFileName) {
		reqHeaders["If-None-Match"] = existingEtag
	}

	resp, err := req_helper.HandleRequestWithHeaders(&c.httpClient, iconUrl, http.MethodGet, constant.TimeOut20s, reqHeaders)
	if err != nil {
		global.LOG.Warnf("[AppStore] request icon failed url=%s, err=%v", iconUrl, err)
		return 0, ""
	}

	if resp.StatusCode == http.StatusNotModified {
		return http.StatusNotModified, ""
	}

	if resp.StatusCode != http.StatusOK {
		global.LOG.Warnf("[AppStore] download icon failed url=%s, status=%d", iconUrl, resp.StatusCode)
		return 0, ""
	}

	if len(resp.Body) == 0 {
		global.LOG.Warnf("[AppStore] download icon empty body url=%s", iconUrl)
		return 0, ""
	}

	if resp.Body[0] == '<' {
		global.LOG.Warnf("[AppStore] download icon got HTML response url=%s", iconUrl)
		return 0, ""
	}

	contentType := resp.Header.Get("Content-Type")
	ct := strings.TrimSpace(strings.Split(contentType, ";")[0])
	if strings.ToLower(ct) != "image/png" {
		global.LOG.Warnf("[AppStore] unexpected icon content-type: %s, expected image/png, url=%s", ct, iconUrl)
	}

	fileName, err := appicon.WriteIconFile(appKey, resp.Body)
	if err != nil {
		global.LOG.Warnf("[AppStore] write icon file failed appKey=%s, err=%v", appKey, err)
		return 0, ""
	}

	newEtag := resp.Header.Get("ETag")
	iconField = appicon.BuildIconField(fileName, newEtag)

	return http.StatusOK, iconField
}

func (a AppService) createSyncAppStoreMetaTask(sharedCtx **appSyncContext) func(t *task.Task) error {
	return func(t *task.Task) (err error) {
		t.LogStart(i18n.GetMsgByKey("SyncAppDetail"))
		ctx := *sharedCtx
		if ctx == nil {
			global.LOG.Warnf("[AppStore] meta sync skipped: shared context is nil")
			t.Log(i18n.GetMsgByKey("SyncAppDetail") + " skipped: shared context is nil")
			return nil
		}

		if ctx.skipMetaSync {
			global.LOG.Infof("[AppStore] meta sync skipped: no update needed")
			t.Log(i18n.GetMsgByKey("SyncAppDetail") + " skipped: no update needed")
			return nil
		}

		if ctx.list == nil || ctx.appsMap == nil {
			global.LOG.Errorf("[AppStore] meta sync failed: shared context data not initialized")
			err := fmt.Errorf("shared context data not initialized")
			t.LogFailedWithErr(i18n.GetMsgByKey("SyncAppDetail"), err)
			return err
		}

		t.Logf("%s: %d apps", i18n.GetMsgByKey("SyncAppDetail"), len(ctx.list.Apps))

		ctx.task = t
		ctx.appTags = make([]*model.AppTag, 0)

		if err = ctx.syncAppIconsAndDetails(); err != nil {
			t.LogFailedWithErr(i18n.GetMsgByKey("SyncAppDetail"), err)
			return err
		}

		if err = ctx.classifyAndPersistApps(); err != nil {
			t.LogFailedWithErr(i18n.GetMsgByKey("PersistAppDetails"), err)
			return err
		}

		global.LOG.Infof("[AppStore] Appstore meta sync completed")
		return nil
	}
}

func (c *appSyncContext) classifyAndPersistApps() (err error) {
	var addCount, updateCount, deleteCount int
	return c.classifyAndPersistAppsWithStats(&addCount, &updateCount, &deleteCount)
}

func (c *appSyncContext) classifyAndPersistAppsWithStats(addCount, updateCount, deleteCount *int) (err error) {
	tags, _ := tagRepo.All()
	var (
		addAppArray    []model.App
		updateAppArray []model.App
		deleteAppArray []model.App
		deleteIds      []uint
		tagMap         = make(map[string]uint, len(tags))
	)

	for _, v := range c.appsMap {
		if v.ID == 0 {
			addAppArray = append(addAppArray, v)
		} else {
			if v.Status == constant.AppTakeDown {
				installs, _ := appInstallRepo.ListBy(context.Background(), appInstallRepo.WithAppId(v.ID))
				if len(installs) > 0 {
					updateAppArray = append(updateAppArray, v)
					continue
				}
				deleteAppArray = append(deleteAppArray, v)
				deleteIds = append(deleteIds, v.ID)
			} else {
				updateAppArray = append(updateAppArray, v)
			}
		}
	}

	if len(addAppArray) > 0 {
		addKeys := make([]string, 0, len(addAppArray))
		for _, app := range addAppArray {
			addKeys = append(addKeys, app.Key)
		}
		existingApps, _ := appRepo.GetBy(appRepo.WithKeyIn(addKeys))
		if len(existingApps) > 0 {
			existingMap := make(map[string]model.App, len(existingApps))
			for _, e := range existingApps {
				existingMap[e.Key] = e
			}
			filteredAdd := make([]model.App, 0, len(addAppArray))
			for _, app := range addAppArray {
				if existing, ok := existingMap[app.Key]; ok {
					app.ID = existing.ID
					if len(app.Details) == 0 {
						app.Details = existing.Details
					}
					updateAppArray = append(updateAppArray, app)
				} else {
					filteredAdd = append(filteredAdd, app)
				}
			}
			addAppArray = filteredAdd
		}
	}

	*addCount = len(addAppArray)
	*updateCount = len(updateAppArray)
	*deleteCount = len(deleteAppArray)

	tx, ctx := getTxAndContext()
	defer func() {
		if err != nil {
			tx.Rollback()
			return
		}
	}()

	if len(addAppArray) > 0 {
		if err = appRepo.BatchCreate(ctx, addAppArray); err != nil {
			return
		}
	}

	if len(deleteAppArray) > 0 {
		if err = appRepo.BatchDelete(ctx, deleteAppArray); err != nil {
			return
		}
		if err = appDetailRepo.DeleteByAppIds(ctx, deleteIds); err != nil {
			return
		}
	}

	for _, tag := range tags {
		tagMap[tag.Key] = tag.ID
	}

	if len(updateAppArray) > 0 {
		for _, update := range updateAppArray {
			if err = appRepo.Save(ctx, &update); err != nil {
				return
			}
		}
	}

	apps := append(addAppArray, updateAppArray...)

	var (
		addDetails    []model.AppDetail
		updateDetails []model.AppDetail
		deleteDetails []model.AppDetail
	)

	totalDetails := 0
	for _, app := range apps {
		for _, tag := range app.TagsKey {
			tagId, ok := tagMap[tag]
			if ok {
				exist, _ := appTagRepo.GetFirst(ctx, appTagRepo.WithByTagID(tagId), appTagRepo.WithByAppID(app.ID))
				if exist == nil {
					c.appTags = append(c.appTags, &model.AppTag{
						AppId: app.ID,
						TagId: tagId,
					})
				}
			}
		}

		for _, d := range app.Details {
			totalDetails++
			d.AppId = app.ID
			if d.ID == 0 {
				addDetails = append(addDetails, d)
			} else {
				if d.Status == constant.AppTakeDown {
					runtime, _ := runtimeRepo.GetFirst(ctx, runtimeRepo.WithDetailId(d.ID))
					if runtime != nil {
						updateDetails = append(updateDetails, d)
						continue
					}
					installs, _ := appInstallRepo.ListBy(ctx, appInstallRepo.WithDetailIdsIn([]uint{d.ID}))
					if len(installs) > 0 {
						updateDetails = append(updateDetails, d)
						continue
					}
					deleteDetails = append(deleteDetails, d)
				} else {
					updateDetails = append(updateDetails, d)
				}
			}
		}
	}

	if len(addDetails) > 0 {
		if err = appDetailRepo.BatchCreate(ctx, addDetails); err != nil {
			return
		}
	}

	if len(deleteDetails) > 0 {
		if err = appDetailRepo.BatchDelete(ctx, deleteDetails); err != nil {
			return
		}
	}

	if len(updateDetails) > 0 {
		for _, u := range updateDetails {
			if err = appDetailRepo.Update(ctx, u); err != nil {
				return
			}
		}
	}

	if len(c.oldAppIds) > 0 {
		if err = appTagRepo.DeleteByAppIds(ctx, deleteIds); err != nil {
			return
		}
	}

	if len(c.appTags) > 0 {
		if err = appTagRepo.BatchCreate(ctx, c.appTags); err != nil {
			return
		}
	}

	if err = tx.Commit().Error; err != nil {
		return
	}
	return nil
}
