package service

import (
	"context"
	"errors"
	"fmt"
	"net"
	"sort"
	"strings"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/buserr"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"gorm.io/gorm"
)

func (a AgentService) BindWebsite(req dto.AgentWebsiteBindReq) error {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
	if err != nil {
		return err
	}
	if agent.WebsiteID != 0 {
		return buserr.New("ErrAgentWebsiteBound")
	}

	website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
	if err != nil {
		return err
	}
	if !isBindableAgentWebsiteType(website.Type) {
		return buserr.New("ErrAgentWebsiteTypeUnsupported")
	}

	boundAgent, err := agentRepo.GetFirst(repo.WithByWebsiteID(req.WebsiteID))
	if err == nil && boundAgent.ID > 0 {
		return buserr.New("ErrAgentWebsiteInUse")
	}
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return err
	}

	agent.WebsiteID = req.WebsiteID
	if err := agentRepo.Save(agent); err != nil {
		return err
	}
	return ensureOpenclawWebsiteAllowedOrigin(agent, &website)
}

func (a AgentService) UnbindWebsite(req dto.AgentIDReq) error {
	agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
	if err != nil {
		return err
	}
	if agent.WebsiteID == 0 {
		return nil
	}

	website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID))
	if err != nil {
		return err
	}
	if website.Type == constant.Deployment {
		return buserr.New("ErrAgentWebsiteUnbindUnsupported")
	}

	agent.WebsiteID = 0
	return agentRepo.Save(agent)
}

func hydrateAgentWebsiteItems(items []dto.AgentItem) error {
	explicitWebsiteMap, err := loadAgentWebsiteMapByID(items)
	if err != nil {
		return err
	}
	websiteDomainMap, err := loadAgentWebsiteDomainMapByWebsiteID(items)
	if err != nil {
		return err
	}
	fillAgentWebsiteItems(items, explicitWebsiteMap, websiteDomainMap)
	return nil
}

func loadAgentWebsiteMapByID(items []dto.AgentItem) (map[uint]model.Website, error) {
	websiteIDs := make([]uint, 0, len(items))
	for _, item := range items {
		if item.WebsiteID > 0 {
			websiteIDs = append(websiteIDs, item.WebsiteID)
		}
	}
	if len(websiteIDs) == 0 {
		return map[uint]model.Website{}, nil
	}
	websites, err := websiteRepo.GetBy(repo.WithByIDs(uniqueUintList(websiteIDs)))
	if err != nil {
		return nil, err
	}
	websiteMap := make(map[uint]model.Website, len(websites))
	for _, website := range websites {
		websiteMap[website.ID] = website
	}
	return websiteMap, nil
}

func loadAgentWebsiteDomainMapByWebsiteID(items []dto.AgentItem) (map[uint][]model.WebsiteDomain, error) {
	websiteIDs := make([]uint, 0, len(items))
	for _, item := range items {
		if item.WebsiteID > 0 {
			websiteIDs = append(websiteIDs, item.WebsiteID)
		}
	}
	if len(websiteIDs) == 0 {
		return map[uint][]model.WebsiteDomain{}, nil
	}
	websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteIds(uniqueUintList(websiteIDs)), repo.WithOrderAsc("id"))
	if err != nil {
		return nil, err
	}
	websiteDomainMap := make(map[uint][]model.WebsiteDomain, len(websiteDomains))
	for _, websiteDomain := range websiteDomains {
		websiteDomainMap[websiteDomain.WebsiteID] = append(websiteDomainMap[websiteDomain.WebsiteID], websiteDomain)
	}
	return websiteDomainMap, nil
}

func loadAgentWebsiteResourceName(website model.Website) (string, error) {
	websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID), repo.WithOrderAsc("id"))
	if err != nil {
		return "", err
	}
	if len(websiteDomains) > 0 {
		return websiteDomains[0].Domain, nil
	}
	if website.PrimaryDomain != "" {
		return website.PrimaryDomain, nil
	}
	return fmt.Sprintf("%d", website.ID), nil
}

func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]model.Website, websiteDomainMap map[uint][]model.WebsiteDomain) {
	for index := range items {
		if items[index].WebsiteID == 0 {
			continue
		}
		website, ok := explicitWebsiteMap[items[index].WebsiteID]
		if !ok {
			items[index].WebsiteID = 0
			items[index].WebsitePrimaryDomain = ""
			items[index].WebsiteType = ""
			items[index].WebsiteProtocol = ""
			continue
		}
		items[index].WebsiteType = website.Type
		items[index].WebsiteProtocol = website.Protocol
		websiteDomains := websiteDomainMap[items[index].WebsiteID]
		if len(websiteDomains) == 0 {
			items[index].WebsitePrimaryDomain = ""
			continue
		}
		items[index].WebsitePrimaryDomain = websiteDomains[0].Domain
	}
}

func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
	websiteMap := make(map[uint]model.Website)
	duplicateAppInstallIDs := make(map[uint]struct{})
	for _, website := range websites {
		if website.AppInstallID == 0 {
			continue
		}
		if _, duplicated := duplicateAppInstallIDs[website.AppInstallID]; duplicated {
			continue
		}
		if _, exists := websiteMap[website.AppInstallID]; exists {
			delete(websiteMap, website.AppInstallID)
			duplicateAppInstallIDs[website.AppInstallID] = struct{}{}
			continue
		}
		websiteMap[website.AppInstallID] = website
	}
	return websiteMap
}

func uniqueUintList(items []uint) []uint {
	itemMap := make(map[uint]struct{}, len(items))
	uniq := make([]uint, 0, len(items))
	for _, item := range items {
		if item == 0 {
			continue
		}
		if _, exists := itemMap[item]; exists {
			continue
		}
		itemMap[item] = struct{}{}
		uniq = append(uniq, item)
	}
	return uniq
}

func isBindableAgentWebsiteType(websiteType string) bool {
	return websiteType == constant.Proxy || websiteType == constant.Static
}

func bindDeploymentWebsiteToAgentByAppInstall(website *model.Website) error {
	if website.ID == 0 || website.Type != constant.Deployment || website.AppInstallID == 0 {
		return nil
	}

	appInstall, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
	if err != nil {
		return err
	}
	if appInstall.App.Key != constant.AppOpenclaw && appInstall.App.Key != constant.AppCopaw && appInstall.App.Key != constant.AppHermesAgent {
		return nil
	}

	agent, err := agentRepo.GetFirst(repo.WithByAppInstallID(website.AppInstallID))
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil
	}
	if err != nil {
		return err
	}
	if agent.WebsiteID != 0 {
		return nil
	}

	agent.WebsiteID = website.ID
	if err := agentRepo.Save(agent); err != nil {
		return err
	}
	return ensureOpenclawWebsiteAllowedOrigin(agent, website)
}

func ensureOpenclawWebsiteAllowedOrigin(agent *model.Agent, website *model.Website) error {
	if agent == nil || website == nil || agent.AgentType != constant.AppOpenclaw {
		return nil
	}

	origins, err := buildWebsiteAllowedOrigins(website)
	if err != nil {
		return err
	}
	if len(origins) == 0 {
		return nil
	}

	install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
	if err != nil {
		return err
	}
	conf, err := readOpenclawConfig(agent.ConfigPath)
	if err != nil {
		return err
	}

	allowedOrigins := extractSecurityConfig(conf).AllowedOrigins
	allowedOrigins, err = normalizeAllowedOrigins(append(allowedOrigins, origins...))
	if err != nil {
		return err
	}
	setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
	if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
		return err
	}
	if err := syncOpenclawAllowedOriginEnv(&install, allowedOrigins); err != nil {
		return err
	}
	return appInstallRepo.Save(context.Background(), &install)
}

func buildWebsiteAllowedOrigins(website *model.Website) ([]string, error) {
	websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID), repo.WithOrderAsc("id"))
	if err != nil {
		return nil, err
	}
	if len(websiteDomains) == 0 {
		return nil, nil
	}
	defaultHTTPSPort := 443
	if strings.EqualFold(website.Protocol, "https") {
		nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
		if err == nil && nginxInstall.HttpsPort > 0 {
			defaultHTTPSPort = nginxInstall.HttpsPort
		}
	}
	return buildWebsiteAllowedOriginsFromDomains(website, websiteDomains, defaultHTTPSPort)
}

func buildWebsiteAllowedOriginsFromDomains(website *model.Website, websiteDomains []model.WebsiteDomain, defaultHTTPSPort int) ([]string, error) {
	if len(websiteDomains) == 0 {
		return nil, nil
	}
	sort.Slice(websiteDomains, func(i, j int) bool {
		return websiteDomains[i].ID < websiteDomains[j].ID
	})
	origins := make([]string, 0, len(websiteDomains))
	for _, websiteDomain := range websiteDomains {
		origin, err := buildWebsiteDomainOrigin(website.Protocol, websiteDomain, defaultHTTPSPort)
		if err != nil {
			return nil, err
		}
		origins = append(origins, origin)
	}
	return origins, nil
}

func buildWebsiteDomainOrigin(protocol string, websiteDomain model.WebsiteDomain, defaultHTTPSPort int) (string, error) {
	origin := strings.ToLower(strings.TrimSpace(protocol)) + "://" + formatWebsiteDomainHost(websiteDomain.Domain)
	switch strings.ToLower(strings.TrimSpace(protocol)) {
	case "http":
		if websiteDomain.Port > 0 && websiteDomain.Port != 80 {
			origin = fmt.Sprintf("%s:%d", origin, websiteDomain.Port)
		}
	case "https":
		port := websiteDomain.Port
		if !websiteDomain.SSL {
			port = defaultHTTPSPort
		}
		if port > 0 && port != 443 {
			origin = fmt.Sprintf("%s:%d", origin, port)
		}
	}
	return normalizeAllowedOrigin(origin)
}

func formatWebsiteDomainHost(domain string) string {
	host := strings.Trim(strings.TrimSpace(domain), "[]")
	if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") {
		return "[" + host + "]"
	}
	return host
}
