package rms

import (
	"bytes"
	"crypto/sha1"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"

	"rmser/internal/domain/catalog"
	"rmser/internal/domain/invoices"
	"rmser/internal/domain/recipes"
	"rmser/pkg/logger"
)

const (
	tokenTTL = 45 * time.Minute // Время жизни токена до принудительного обновления
)

// ClientI интерфейс
type ClientI interface {
	Auth() error
	Logout() error
	FetchCatalog() ([]catalog.Product, error)
	FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error)
	FetchInvoices(from, to time.Time) ([]invoices.Invoice, error)
	FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error)
	CreateIncomingInvoice(inv invoices.Invoice) (string, error)
}

type Client struct {
	baseURL      string
	login        string
	passwordHash string
	httpClient   *http.Client

	// Защита токена для конкурентного доступа
	mu             sync.RWMutex
	token          string
	tokenCreatedAt time.Time
}

func NewClient(baseURL, login, password string) *Client {
	h := sha1.New()
	h.Write([]byte(password))
	passHash := fmt.Sprintf("%x", h.Sum(nil))

	return &Client{
		baseURL:      baseURL,
		login:        login,
		passwordHash: passHash,
		httpClient:   &http.Client{Timeout: 60 * time.Second},
	}
}

// Auth выполняет вход
func (c *Client) Auth() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	return c.authUnsafe()
}

// authUnsafe - внутренняя логика авторизации без блокировок (вызывается внутри Lock)
func (c *Client) authUnsafe() error {
	endpoint := c.baseURL + "/resto/api/auth"

	data := url.Values{}
	data.Set("login", c.login)
	data.Set("pass", c.passwordHash)

	req, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
	if err != nil {
		return fmt.Errorf("ошибка создания запроса auth: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("ошибка сети auth: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("ошибка авторизации (code %d): %s", resp.StatusCode, string(body))
	}

	c.token = string(body)
	c.tokenCreatedAt = time.Now() // Запоминаем время получения
	logger.Log.Info("RMS: Успешная авторизация", zap.String("token_preview", c.token[:5]+"..."))
	return nil
}

// Logout освобождает лицензию
func (c *Client) Logout() error {
	c.mu.Lock()
	defer c.mu.Unlock()

	return c.logoutUnsafe()
}

// logoutUnsafe - внутренняя логика логаута
func (c *Client) logoutUnsafe() error {
	if c.token == "" {
		return nil
	}

	endpoint := c.baseURL + "/resto/api/logout"
	data := url.Values{}
	data.Set("key", c.token)

	req, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
	if err == nil {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		resp, err := c.httpClient.Do(req)
		if err == nil {
			defer resp.Body.Close()
			if resp.StatusCode == http.StatusOK {
				logger.Log.Info("RMS: Токен освобожден")
			} else {
				logger.Log.Warn("RMS: Ошибка освобождения токена", zap.Int("code", resp.StatusCode))
			}
		}
	}

	// Сбрасываем токен в любом случае, даже если запрос не прошел (он все равно протухнет)
	c.token = ""
	c.tokenCreatedAt = time.Time{}
	return nil
}

// ensureToken проверяет срок жизни токена и обновляет его при необходимости
func (c *Client) ensureToken() error {
	c.mu.RLock()
	token := c.token
	createdAt := c.tokenCreatedAt
	c.mu.RUnlock()

	// Если токена нет или он протух
	if token == "" || time.Since(createdAt) > tokenTTL {
		c.mu.Lock()
		defer c.mu.Unlock()

		// Double check locking (вдруг другая горутина уже обновила)
		if c.token != "" && time.Since(c.tokenCreatedAt) <= tokenTTL {
			return nil
		}

		if c.token != "" {
			logger.Log.Info("RMS: Время жизни токена истекло (>45 мин), пересоздание...")
			_ = c.logoutUnsafe() // Пытаемся освободить старый
		}

		return c.authUnsafe()
	}
	return nil
}

// doRequest выполняет запрос с автоматическим управлением токеном
func (c *Client) doRequest(method, path string, queryParams map[string]string) (*http.Response, error) {
	// 1. Проверка времени жизни (45 минут)
	if err := c.ensureToken(); err != nil {
		return nil, err
	}

	// Читаем токен под RLock
	c.mu.RLock()
	currentToken := c.token
	c.mu.RUnlock()

	buildURL := func() string {
		u, _ := url.Parse(c.baseURL + path)
		q := u.Query()
		q.Set("key", currentToken)
		for k, v := range queryParams {
			q.Set(k, v)
		}
		u.RawQuery = q.Encode()
		return u.String()
	}

	req, _ := http.NewRequest(method, buildURL(), nil)
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

	// 2. Реактивная обработка 401 (если сервер перезагрузился или убил сессию раньше времени)
	if resp.StatusCode == http.StatusUnauthorized {
		resp.Body.Close()
		logger.Log.Warn("RMS: Получен 401 Unauthorized, принудительная ре-авторизация...")

		c.mu.Lock()
		// Сбрасываем токен и логинимся заново
		c.token = ""
		authErr := c.authUnsafe()
		c.mu.Unlock()

		if authErr != nil {
			return nil, authErr
		}

		// Повторяем запрос с новым токеном
		c.mu.RLock()
		currentToken = c.token
		c.mu.RUnlock()

		req, _ = http.NewRequest(method, buildURL(), nil)
		return c.httpClient.Do(req)
	}

	if resp.StatusCode != http.StatusOK {
		defer resp.Body.Close()
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("api error: code=%d, body=%s", resp.StatusCode, string(body))
	}

	return resp, nil
}

// --- Методы получения данных (без изменений логики парсинга) ---

func (c *Client) FetchCatalog() ([]catalog.Product, error) {
	var products []catalog.Product

	// Группы
	respGroups, err := c.doRequest("GET", "/resto/api/v2/entities/products/group/list", map[string]string{
		"includeDeleted": "true",
	})
	if err != nil {
		return nil, fmt.Errorf("get groups error: %w", err)
	}
	defer respGroups.Body.Close()

	var groupDTOs []GroupDTO
	if err := json.NewDecoder(respGroups.Body).Decode(&groupDTOs); err != nil {
		return nil, fmt.Errorf("json decode groups error: %w", err)
	}

	// Товары
	respProds, err := c.doRequest("GET", "/resto/api/v2/entities/products/list", map[string]string{
		"includeDeleted": "true",
	})
	if err != nil {
		return nil, fmt.Errorf("get products error: %w", err)
	}
	defer respProds.Body.Close()

	var prodDTOs []ProductDTO
	if err := json.NewDecoder(respProds.Body).Decode(&prodDTOs); err != nil {
		return nil, fmt.Errorf("json decode products error: %w", err)
	}

	// Маппинг групп
	for _, g := range groupDTOs {
		id, _ := uuid.Parse(g.ID)
		var parentID *uuid.UUID
		if g.ParentID != nil {
			if pid, err := uuid.Parse(*g.ParentID); err == nil {
				parentID = &pid
			}
		}
		products = append(products, catalog.Product{
			ID:        id,
			ParentID:  parentID,
			Name:      g.Name,
			Num:       g.Num,
			Code:      g.Code,
			Type:      "GROUP",
			IsDeleted: g.Deleted,
		})
	}

	// Маппинг товаров
	for _, p := range prodDTOs {
		id, _ := uuid.Parse(p.ID)
		var parentID *uuid.UUID
		if p.ParentID != nil {
			if pid, err := uuid.Parse(*p.ParentID); err == nil {
				parentID = &pid
			}
		}
		products = append(products, catalog.Product{
			ID:           id,
			ParentID:     parentID,
			Name:         p.Name,
			Num:          p.Num,
			Code:         p.Code,
			Type:         p.Type,
			UnitWeight:   decimal.NewFromFloat(p.UnitWeight),
			UnitCapacity: decimal.NewFromFloat(p.UnitCapacity),
			IsDeleted:    p.Deleted,
		})
	}

	return products, nil
}

func (c *Client) FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error) {
	params := map[string]string{
		"dateFrom": dateFrom.Format("2006-01-02"),
	}
	if !dateTo.IsZero() {
		params["dateTo"] = dateTo.Format("2006-01-02")
	}

	resp, err := c.doRequest("GET", "/resto/api/v2/assemblyCharts/getAll", params)
	if err != nil {
		return nil, fmt.Errorf("get recipes error: %w", err)
	}
	defer resp.Body.Close()

	var apiResp AssemblyChartsResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
		return nil, fmt.Errorf("json decode recipes error: %w", err)
	}

	var allrecipes []recipes.Recipe
	for _, chart := range apiResp.AssemblyCharts {
		rID, _ := uuid.Parse(chart.ID)
		pID, _ := uuid.Parse(chart.AssembledProductID)

		df, _ := time.Parse("2006-01-02", chart.DateFrom)
		var dt *time.Time
		if chart.DateTo != nil {
			if t, err := time.Parse("2006-01-02", *chart.DateTo); err == nil {
				dt = &t
			}
		}

		var items []recipes.RecipeItem
		for _, item := range chart.Items {
			iPID, _ := uuid.Parse(item.ProductID)

			// FIX: Генерируем уникальный ID для каждой строки в нашей БД,
			// чтобы избежать конфликтов PK при переиспользовании строк в iiko.
			items = append(items, recipes.RecipeItem{
				ID:        uuid.New(),
				RecipeID:  rID,
				ProductID: iPID,
				AmountIn:  decimal.NewFromFloat(item.AmountIn),
				AmountOut: decimal.NewFromFloat(item.AmountOut),
			})
		}

		allrecipes = append(allrecipes, recipes.Recipe{
			ID:        rID,
			ProductID: pID,
			DateFrom:  df,
			DateTo:    dt,
			Items:     items,
		})
	}

	return allrecipes, nil
}

func (c *Client) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) {
	params := map[string]string{
		"from":        from.Format("2006-01-02"),
		"to":          to.Format("2006-01-02"),
		"currentYear": "false",
	}

	resp, err := c.doRequest("GET", "/resto/api/documents/export/incomingInvoice", params)
	if err != nil {
		return nil, fmt.Errorf("get invoices error: %w", err)
	}
	defer resp.Body.Close()

	var xmlData IncomingInvoiceListXML
	if err := xml.NewDecoder(resp.Body).Decode(&xmlData); err != nil {
		return nil, fmt.Errorf("xml decode invoices error: %w", err)
	}

	var allinvoices []invoices.Invoice
	for _, doc := range xmlData.Documents {
		docID, _ := uuid.Parse(doc.ID)
		supID, _ := uuid.Parse(doc.Supplier)
		storeID, _ := uuid.Parse(doc.DefaultStore)
		dateInc, _ := time.Parse("2006-01-02T15:04:05", doc.DateIncoming)

		var items []invoices.InvoiceItem
		for _, it := range doc.Items {
			pID, _ := uuid.Parse(it.Product)
			items = append(items, invoices.InvoiceItem{
				InvoiceID: docID,
				ProductID: pID,
				Amount:    decimal.NewFromFloat(it.Amount),
				Price:     decimal.NewFromFloat(it.Price),
				Sum:       decimal.NewFromFloat(it.Sum),
				VatSum:    decimal.NewFromFloat(it.VatSum),
			})
		}

		allinvoices = append(allinvoices, invoices.Invoice{
			ID:             docID,
			DocumentNumber: doc.DocumentNumber,
			DateIncoming:   dateInc,
			SupplierID:     supID,
			DefaultStoreID: storeID,
			Status:         doc.Status,
			Items:          items,
		})
	}

	return allinvoices, nil
}

// FetchStoreOperations загружает складской отчет по ID пресета
func (c *Client) FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error) {
	params := map[string]string{
		"presetId": presetID,
		"dateFrom": from.Format("02.01.2006"), // В документации формат DD.MM.YYYY
		"dateTo":   to.Format("02.01.2006"),
	}

	resp, err := c.doRequest("GET", "/resto/api/reports/storeOperations", params)
	if err != nil {
		return nil, fmt.Errorf("fetch store operations error: %w", err)
	}
	defer resp.Body.Close()

	var report StoreReportResponse
	if err := xml.NewDecoder(resp.Body).Decode(&report); err != nil {
		// Иногда RMS возвращает пустой ответ или ошибку текстом при отсутствии данных
		return nil, fmt.Errorf("xml decode store operations error: %w", err)
	}

	return report.Items, nil
}

// CreateIncomingInvoice отправляет накладную в iiko
func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
	// 1. Маппинг Domain -> XML DTO
	reqDTO := IncomingInvoiceImportXML{
		DocumentNumber: inv.DocumentNumber,
		DateIncoming:   inv.DateIncoming.Format("02.01.2006"),
		DefaultStore:   inv.DefaultStoreID.String(),
		Supplier:       inv.SupplierID.String(),
		Status:         "NEW",
		Comment:        "Loaded via RMSER OCR",
	}

	if inv.ID != uuid.Nil {
		reqDTO.ID = inv.ID.String()
	}

	for i, item := range inv.Items {
		amount, _ := item.Amount.Float64()
		price, _ := item.Price.Float64()
		sum, _ := item.Sum.Float64()

		reqDTO.ItemsWrapper.Items = append(reqDTO.ItemsWrapper.Items, IncomingInvoiceImportItemXML{
			ProductID: item.ProductID.String(),
			Amount:    amount,
			Price:     price,
			Sum:       sum,
			Num:       i + 1,
			Store:     inv.DefaultStoreID.String(),
		})
	}

	// 2. Маршалинг в XML
	xmlBytes, err := xml.Marshal(reqDTO)
	if err != nil {
		return "", fmt.Errorf("xml marshal error: %w", err)
	}
	// Добавляем XML header вручную
	xmlPayload := []byte(xml.Header + string(xmlBytes))

	// 3. Получение токена
	if err := c.ensureToken(); err != nil {
		return "", err
	}

	c.mu.RLock()
	token := c.token
	c.mu.RUnlock()

	// 4. Формирование URL
	endpoint, _ := url.Parse(c.baseURL + "/resto/api/documents/import/incomingInvoice")
	q := endpoint.Query()
	q.Set("key", token)
	endpoint.RawQuery = q.Encode()

	fullURL := endpoint.String()

	// --- ЛОГИРОВАНИЕ ЗАПРОСА (URL + BODY) ---
	// Логируем как Info, чтобы точно увидеть в консоли при отладке
	logger.Log.Info("RMS POST Request Debug",
		zap.String("method", "POST"),
		zap.String("url", fullURL),
		zap.String("body_payload", string(xmlPayload)),
	)
	// ----------------------------------------

	// 5. Отправка
	req, err := http.NewRequest("POST", fullURL, bytes.NewReader(xmlPayload))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/xml")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("network error: %w", err)
	}
	defer resp.Body.Close()

	// Читаем ответ
	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	// Логируем ответ для симметрии
	logger.Log.Info("RMS POST Response Debug",
		zap.Int("status_code", resp.StatusCode),
		zap.String("response_body", string(respBody)),
	)

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("http error %d: %s", resp.StatusCode, string(respBody))
	}

	var result DocumentValidationResult
	if err := xml.Unmarshal(respBody, &result); err != nil {
		return "", fmt.Errorf("xml response unmarshal error: %w", err)
	}

	if !result.Valid {
		logger.Log.Warn("RMS Invoice Import Failed",
			zap.String("error", result.ErrorMessage),
			zap.String("additional", result.AdditionalInfo),
		)
		return "", fmt.Errorf("iiko validation failed: %s (info: %s)", result.ErrorMessage, result.AdditionalInfo)
	}

	return result.DocumentNumber, nil
}
