package session

import (
	"context"
	"fmt"
	"hash/crc32"
	"net"
	"net/http"
	"strconv"
	"strings"
	"time"

	pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie"

	"gorm.io/gorm"
)

type Service struct {
	db     *gorm.DB
	prefix string
}

type defaults struct {
	LanguageID  int64
	CurrencyID  int64
	ShopID      int64
	ShopGroupID int64
	CountryISO  string
}

func NewService(db *gorm.DB, prefix string) *Service {
	return &Service{db: db, prefix: prefix}
}

func (s *Service) NewAnonymous(ctx context.Context, req *http.Request, cookieName string) (*pscookie.SessionContext, error) {
	if s == nil || s.db == nil {
		return nil, fmt.Errorf("prestashop session service is not initialized")
	}

	def, err := s.loadDefaults(ctx)
	if err != nil {
		return nil, err
	}

	guestID, err := s.insertGuest(ctx)
	if err != nil {
		return nil, err
	}

	connectionID, err := s.insertConnection(ctx, def, guestID, req)
	if err != nil {
		return nil, err
	}

	now := time.Now().UTC()
	values := map[string]string{
		"checksum":         anonymousChecksum(guestID, connectionID, def.LanguageID, def.CurrencyID, def.ShopID),
		"date_add":         now.Format("2006-01-02 15:04:05"),
		"id_cart":          "",
		"id_connections":   strconv.FormatInt(connectionID, 10),
		"id_currency":      strconv.FormatInt(def.CurrencyID, 10),
		"id_guest":         strconv.FormatInt(guestID, 10),
		"id_lang":          strconv.FormatInt(def.LanguageID, 10),
		"id_language":      strconv.FormatInt(def.LanguageID, 10),
		"iso_code_country": def.CountryISO,
	}
	orderedKeys := []string{
		"date_add",
		"id_lang",
		"id_cart",
		"id_language",
		"iso_code_country",
		"id_currency",
		"id_guest",
		"id_connections",
		"checksum",
	}

	if def.ShopID > 0 {
		values["id_shop"] = strconv.FormatInt(def.ShopID, 10)
		orderedKeys = append(orderedKeys[:6], append([]string{"id_shop"}, orderedKeys[6:]...)...)
	}

	return &pscookie.SessionContext{
		CookieName:  cookieName,
		LanguageID:  int64Ptr(def.LanguageID),
		CurrencyID:  int64Ptr(def.CurrencyID),
		ShopID:      int64Ptr(def.ShopID),
		GuestID:     int64Ptr(guestID),
		IsLoggedIn:  false,
		Values:      values,
		OrderedKeys: orderedKeys,
		ParseStatus: pscookie.ParseStatusAnonymous,
	}, nil
}

func (s *Service) loadDefaults(ctx context.Context) (*defaults, error) {
	def := &defaults{
		LanguageID:  1,
		CurrencyID:  1,
		ShopID:      1,
		ShopGroupID: 1,
		CountryISO:  "US",
	}

	configTable := s.prefix + "configuration"
	shopTable := s.prefix + "shop"
	countryTable := s.prefix + "country"

	var configs []struct {
		Name  string
		Value string
	}
	configQuery := fmt.Sprintf("SELECT name, value FROM %s WHERE name IN ('PS_LANG_DEFAULT', 'PS_CURRENCY_DEFAULT', 'PS_COUNTRY_DEFAULT')", configTable)
	if err := s.db.WithContext(ctx).Raw(configQuery).Scan(&configs).Error; err != nil {
		return nil, err
	}

	countryID := int64(0)
	for _, cfg := range configs {
		switch cfg.Name {
		case "PS_LANG_DEFAULT":
			if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 {
				def.LanguageID = parsed
			}
		case "PS_CURRENCY_DEFAULT":
			if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 {
				def.CurrencyID = parsed
			}
		case "PS_COUNTRY_DEFAULT":
			if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 {
				countryID = parsed
			}
		}
	}

	var shop struct {
		ID      int64 `gorm:"column:id_shop"`
		GroupID int64 `gorm:"column:id_shop_group"`
	}
	shopQuery := fmt.Sprintf("SELECT id_shop, id_shop_group FROM %s ORDER BY id_shop LIMIT 1", shopTable)
	if err := s.db.WithContext(ctx).Raw(shopQuery).Scan(&shop).Error; err != nil {
		return nil, err
	}
	if shop.ID > 0 {
		def.ShopID = shop.ID
	}
	if shop.GroupID > 0 {
		def.ShopGroupID = shop.GroupID
	}

	if countryID > 0 {
		var country struct {
			ISOCode string `gorm:"column:iso_code"`
		}
		countryQuery := fmt.Sprintf("SELECT iso_code FROM %s WHERE id_country = ? LIMIT 1", countryTable)
		if err := s.db.WithContext(ctx).Raw(countryQuery, countryID).Scan(&country).Error; err != nil {
			return nil, err
		}
		if country.ISOCode != "" {
			def.CountryISO = country.ISOCode
		}
	}

	return def, nil
}

func (s *Service) insertGuest(ctx context.Context) (int64, error) {
	sqlDB, err := s.db.DB()
	if err != nil {
		return 0, fmt.Errorf("resolve sql db for guest insert: %w", err)
	}
	tableName := s.prefix + "guest"
	columns, values, err := s.guestInsert(ctx)
	if err != nil {
		return 0, err
	}
	query := insertQuery(tableName, columns)
	result, err := sqlDB.ExecContext(ctx, query, values...)
	if err != nil {
		return 0, fmt.Errorf("insert guest: %w", err)
	}
	id, err := result.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("guest last insert id: %w", err)
	}
	return id, nil
}

func (s *Service) insertConnection(ctx context.Context, def *defaults, guestID int64, req *http.Request) (int64, error) {
	sqlDB, err := s.db.DB()
	if err != nil {
		return 0, fmt.Errorf("resolve sql db for connection insert: %w", err)
	}
	tableName := s.prefix + "connections"
	columns, values, err := s.connectionInsert(ctx, def, guestID, req)
	if err != nil {
		return 0, err
	}
	query := insertQuery(tableName, columns)
	result, err := sqlDB.ExecContext(ctx, query, values...)
	if err != nil {
		return 0, fmt.Errorf("insert connection: %w", err)
	}
	id, err := result.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("connection last insert id: %w", err)
	}
	return id, nil
}

func (s *Service) guestInsert(ctx context.Context) ([]string, []any, error) {
	available, err := s.tableColumns(ctx, s.prefix+"guest")
	if err != nil {
		return nil, nil, fmt.Errorf("load guest columns: %w", err)
	}

	columns := make([]string, 0)
	values := make([]any, 0)
	addColumn := func(name string, value any) {
		if available[name] {
			columns = append(columns, name)
			values = append(values, value)
		}
	}

	addColumn("id_customer", 0)
	addColumn("id_operating_system", 0)
	addColumn("id_web_browser", 0)
	addColumn("javascript", 0)
	addColumn("screen_resolution_x", 0)
	addColumn("screen_resolution_y", 0)
	addColumn("screen_color", 0)
	addColumn("sun_java", 0)
	addColumn("adobe_flash", 0)
	addColumn("adobe_director", 0)
	addColumn("apple_quicktime", 0)
	addColumn("real_player", 0)
	addColumn("windows_media", 0)
	addColumn("accept_language", "")
	addColumn("mobile_theme", 0)

	return columns, values, nil
}

func (s *Service) connectionInsert(ctx context.Context, def *defaults, guestID int64, req *http.Request) ([]string, []any, error) {
	available, err := s.tableColumns(ctx, s.prefix+"connections")
	if err != nil {
		return nil, nil, fmt.Errorf("load connections columns: %w", err)
	}

	now := time.Now().UTC().Format("2006-01-02 15:04:05")
	columns := make([]string, 0)
	values := make([]any, 0)
	addColumn := func(name string, value any) {
		if available[name] {
			columns = append(columns, name)
			values = append(values, value)
		}
	}

	addColumn("id_guest", guestID)
	addColumn("id_shop", def.ShopID)
	addColumn("id_shop_group", def.ShopGroupID)
	addColumn("id_page", 0)
	addColumn("ip_address", ipAsUint32(req))
	addColumn("date_add", now)
	addColumn("date_upd", now)
	addColumn("http_referer", referer(req))
	addColumn("request_uri", requestURI(req))

	return columns, values, nil
}

func (s *Service) tableColumns(ctx context.Context, tableName string) (map[string]bool, error) {
	type columnRow struct {
		ColumnName string `gorm:"column:COLUMN_NAME"`
	}

	var rows []columnRow
	query := `
SELECT COLUMN_NAME
FROM information_schema.columns
WHERE table_schema = DATABASE()
  AND table_name = ?
`
	if err := s.db.WithContext(ctx).Raw(query, tableName).Scan(&rows).Error; err != nil {
		return nil, err
	}

	columns := make(map[string]bool, len(rows))
	for _, row := range rows {
		columns[row.ColumnName] = true
	}
	return columns, nil
}

func insertQuery(tableName string, columns []string) string {
	if len(columns) == 0 {
		return fmt.Sprintf("INSERT INTO %s () VALUES ()", tableName)
	}
	return fmt.Sprintf(
		"INSERT INTO %s (%s) VALUES (%s)",
		tableName,
		strings.Join(columns, ", "),
		placeholders(len(columns)),
	)
}

func placeholders(n int) string {
	parts := make([]string, n)
	for i := range parts {
		parts[i] = "?"
	}
	return strings.Join(parts, ", ")
}

func referer(req *http.Request) string {
	if req == nil {
		return ""
	}
	return req.Referer()
}

func requestURI(req *http.Request) string {
	if req == nil || req.URL == nil {
		return ""
	}
	return req.URL.RequestURI()
}

func ipAsUint32(req *http.Request) uint32 {
	if req == nil {
		return 0
	}
	raw := req.Header.Get("X-Forwarded-For")
	if raw == "" {
		raw = req.RemoteAddr
	}
	if strings.Contains(raw, ",") {
		raw = strings.TrimSpace(strings.Split(raw, ",")[0])
	}
	host := raw
	if parsedHost, _, err := net.SplitHostPort(raw); err == nil {
		host = parsedHost
	}
	ip := net.ParseIP(host)
	if ip == nil {
		return 0
	}
	ip = ip.To4()
	if ip == nil {
		return 0
	}
	return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
}

func anonymousChecksum(values ...int64) string {
	buf := make([]byte, 0, len(values)*8)
	for _, v := range values {
		buf = strconv.AppendInt(buf, v, 10)
		buf = append(buf, '|')
	}
	return strconv.FormatUint(uint64(crc32.ChecksumIEEE(buf)), 10)
}

func int64Ptr(value int64) *int64 {
	if value == 0 {
		return nil
	}
	v := value
	return &v
}
