package lkemulator

import (
	"context"
	"crypto/rand"
	"embed"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"
)

//go:embed web/templates/*.html
var tplFS embed.FS

// ServerConfig — настройки эмулятора ЛК.
type ServerConfig struct {
	Addr          string
	GatewayURL    string
	SelfPublicURL string // адрес, который мы сообщим gateway для callback'ов
}

// Server — HTTP-сервер эмулятора.
type Server struct {
	cfg              ServerConfig
	store            *Store
	gw               *GatewayClient
	home, new, claim *template.Template
	srv              *http.Server
	mux              *http.ServeMux
}

// templateFuncs — функции, доступные внутри шаблонов эмулятора
// (русификация статусов).
var templateFuncs = template.FuncMap{
	"ruState": ruState,
}

// ruState — те же значения, что и в lkgateway.russianState. Дублирование
// допустимое: lkemulator — отдельный пакет и не зависит от lkgateway.
func ruState(s string) string {
	switch s {
	case "draft":
		return "Черновик"
	case "validated":
		return "Валидирована"
	case "submitted_to_nsd":
		return "Отправлена в НРД"
	case "awaiting_decision":
		return "Ожидает решение"
	case "confirmed":
		return "Подтверждена"
	case "awaiting_sub16":
		return "Ожидает SUB16"
	case "done":
		return "Завершена"
	case "rejected":
		return "Отклонена"
	case "timed_out":
		return "Таймаут SLA"
	case "manual_approval":
		return "На ручном разборе"
	}
	return s
}

// NewServer собирает Server.
func NewServer(cfg ServerConfig) (*Server, error) {
	parse := func(content string) (*template.Template, error) {
		return template.New("layout.html").Funcs(templateFuncs).ParseFS(tplFS, "web/templates/layout.html", "web/templates/"+content)
	}
	home, err := parse("home.html")
	if err != nil {
		return nil, fmt.Errorf("parse home: %w", err)
	}
	newTpl, err := parse("new.html")
	if err != nil {
		return nil, fmt.Errorf("parse new: %w", err)
	}
	claimTpl, err := parse("claim.html")
	if err != nil {
		return nil, fmt.Errorf("parse claim: %w", err)
	}
	s := &Server{
		cfg:   cfg,
		store: NewStore(),
		gw:    NewGatewayClient(cfg.GatewayURL),
		home:  home,
		new:   newTpl,
		claim: claimTpl,
		mux:   http.NewServeMux(),
	}
	s.routes()
	s.srv = &http.Server{
		Addr:              cfg.Addr,
		Handler:           s.mux,
		ReadHeaderTimeout: 5 * time.Second,
	}
	return s, nil
}

// Run поднимает сервер, регистрирует свой URL у gateway, ждёт ctx.Done().
func (s *Server) Run(ctx context.Context) error {
	go func() {
		// Дать серверу подняться, потом попытаться зарегистрировать callback URL.
		time.Sleep(200 * time.Millisecond)
		if s.cfg.SelfPublicURL != "" {
			if err := s.gw.SetCallbackURL(ctx, s.cfg.SelfPublicURL); err != nil {
				log.Printf("lk-emulator: не получилось зарегистрировать callback URL: %v", err)
			} else {
				log.Printf("lk-emulator: callback URL %s зарегистрирован в lk-gateway", s.cfg.SelfPublicURL)
			}
		}
	}()

	errCh := make(chan error, 1)
	go func() {
		log.Printf("lk-emulator: listen %s", s.cfg.Addr)
		errCh <- s.srv.ListenAndServe()
	}()

	select {
	case <-ctx.Done():
		shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_ = s.srv.Shutdown(shutCtx)
		return nil
	case err := <-errCh:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return err
	}
}

// Mux возвращает обработчик (для httptest).
func (s *Server) Mux() http.Handler { return s.mux }

// Store возвращает store (для тестов).
func (s *Server) Store() *Store { return s.store }

func (s *Server) routes() {
	s.mux.HandleFunc("/", s.handleHome)
	s.mux.HandleFunc("/new", s.handleNew)
	s.mux.HandleFunc("/claims/", s.handleClaim)
	s.mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})
	// Принимаем callback'и от lk-gateway (PATCH).
	s.mux.HandleFunc("/api/v1/back_office/claims/", s.handleCallback)
}

type pageData struct {
	Title       string
	Active      string
	GatewayURL  string
	AutoRefresh bool
	Flash       string
	Error       string
}

func (s *Server) basePage(title, active string, autoRefresh bool) pageData {
	return pageData{Title: title, Active: active, GatewayURL: s.cfg.GatewayURL, AutoRefresh: autoRefresh}
}

type homeData struct {
	pageData
	Claims []*Claim
}

func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	data := homeData{
		pageData: s.basePage("Мои заявки", "home", true),
		Claims:   s.store.All(),
	}
	if msg := r.URL.Query().Get("flash"); msg != "" {
		data.Flash = msg
	}
	s.renderTpl(w, s.home, data)
}

// clientView — DTO от gateway для выпадающего списка инвесторов.
type clientView struct {
	ID         string `json:"ID"`
	LastName   string `json:"LastName"`
	FirstName  string `json:"FirstName"`
	MiddleName string `json:"MiddleName"`
}

type newData struct {
	pageData
	Clients []clientView
}

func (s *Server) handleNew(w http.ResponseWriter, r *http.Request) {
	if r.Method == http.MethodGet {
		clients, err := s.fetchClients(r.Context())
		if err != nil {
			s.renderTpl(w, s.new, newData{pageData: s.basePage("Новая заявка", "new", false), Clients: nil})
			return
		}
		s.renderTpl(w, s.new, newData{pageData: s.basePage("Новая заявка", "new", false), Clients: clients})
		return
	}
	if r.Method != http.MethodPost {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	if err := s.submitNew(r); err != nil {
		clients, _ := s.fetchClients(r.Context())
		d := newData{pageData: s.basePage("Новая заявка", "new", false), Clients: clients}
		d.Error = err.Error()
		s.renderTpl(w, s.new, d)
		return
	}
	http.Redirect(w, r, "/?flash=Заявка+отправлена+в+lk-gateway.+Ждём+callback...", http.StatusSeeOther)
}

func (s *Server) submitNew(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return fmt.Errorf("разбор формы: %w", err)
	}
	investorID := r.FormValue("investor_id")
	if investorID == "" {
		return errors.New("укажите инвестора")
	}
	clients, err := s.fetchClients(r.Context())
	if err != nil {
		return fmt.Errorf("список инвесторов: %w", err)
	}
	investorName := ""
	for _, c := range clients {
		if c.ID == investorID {
			investorName = strings.TrimSpace(c.LastName + " " + c.FirstName + " " + c.MiddleName)
			break
		}
	}

	tInn := r.FormValue("transferring_depository_inn")
	rInn := r.FormValue("receiving_depository_inn")
	secCode := r.FormValue("security_code")
	isin := r.FormValue("isin")
	qStr := r.FormValue("quantity")
	whole, err := strconv.ParseUint(qStr, 10, 64)
	if err != nil || whole == 0 {
		return errors.New("количество должно быть положительным целым")
	}

	body := map[string]any{
		"investor": map[string]any{
			"id":         investorID,
			"last_name":  splitFio(investorName, 0),
			"first_name": splitFio(investorName, 1),
			"document": map[string]any{
				"document_type": "21",
				"series":        "4512",
				"number":        "654321",
			},
		},
		"transferring_depository_inn": tInn,
		"receiving_depository_inn":    rInn,
		"securities": []any{
			map[string]any{
				"security_code":    secCode,
				"security_details": map[string]any{"isin": isin},
				"quantity":         map[string]any{"whole": whole},
				"settlement_accounts": []any{
					map[string]any{
						"settlement_requisites_inn": "7702070139",
						"settlement_location": map[string]any{
							"deponent_code": "DP789456",
							"account_id":    "31MC0021900000F01",
							"section_id":    "P001",
						},
					},
				},
			},
		},
		"signed_document":  base64.StdEncoding.EncodeToString([]byte("<xml>demo</xml>")),
		"signature_format": "XMLDSig-GOST",
	}
	if code := r.FormValue("cost_info_code"); code != "" {
		body["cost_info"] = map[string]any{"yes": map[string]any{"code": code}}
	} else {
		body["cost_info"] = map[string]any{"no": map[string]any{}}
	}
	if iiaType := r.FormValue("iia_type"); iiaType != "" {
		body["iia_agreement"] = map[string]any{
			"agreement_type":   iiaType,
			"agreement_number": r.FormValue("iia_number"),
			"agreement_date":   r.FormValue("iia_date"),
			"broker_inn":       r.FormValue("iia_broker_inn"),
		}
	}

	resp, err := s.gw.CreateClaim(r.Context(), body)
	if err != nil {
		return err
	}
	gwID, _ := resp["id"].(string)
	status, _ := resp["status"].(string)

	c := &Claim{
		ID:                        randomID(),
		GatewayID:                 gwID,
		Status:                    status,
		LocalStatus:               "submitted",
		CreatedAt:                 time.Now().UTC(),
		UpdatedAt:                 time.Now().UTC(),
		InvestorName:              investorName,
		TransferringDepositoryINN: tInn,
		ReceivingDepositoryINN:    rInn,
		SecuritiesCount:           1,
		RawRequest:                body,
		RawResponse:               resp,
	}
	s.store.Add(c)
	return nil
}

type claimPage struct {
	pageData
	Claim          *Claim
	PrettyRequest  string
	PrettyResponse string
}

func (s *Server) handleClaim(w http.ResponseWriter, r *http.Request) {
	id := strings.TrimPrefix(r.URL.Path, "/claims/")
	if id == "" || strings.Contains(id, "/") {
		http.NotFound(w, r)
		return
	}
	c := s.store.Get(id)
	if c == nil {
		http.NotFound(w, r)
		return
	}
	d := claimPage{
		pageData: s.basePage("Заявка", "home", c.LastCallback == nil),
		Claim:    c,
	}
	if b, err := json.MarshalIndent(c.RawRequest, "", "  "); err == nil {
		d.PrettyRequest = string(b)
	}
	if b, err := json.MarshalIndent(c.RawResponse, "", "  "); err == nil {
		d.PrettyResponse = string(b)
	}
	s.renderTpl(w, s.claim, d)
}

// handleCallback принимает PATCH /api/v1/back_office/claims/{id} от lk-gateway.
func (s *Server) handleCallback(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPatch {
		http.Error(w, "method", http.StatusMethodNotAllowed)
		return
	}
	gwID := strings.TrimPrefix(r.URL.Path, "/api/v1/back_office/claims/")
	if gwID == "" {
		http.Error(w, "id required", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()
	raw, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	var payload struct {
		NewStatus  string    `json:"new_status"`
		ReasonCode string    `json:"reason_code"`
		ReasonText string    `json:"reason_text"`
		UpdatedAt  time.Time `json:"updated_at"`
		NSDResp    *struct {
			GUID       string `json:"guid"`
			StatusCode string `json:"status_code"`
			Responses  []struct {
				ReferenceID string `json:"reference_id"`
				Code        string `json:"code"`
				Text        string `json:"text"`
			} `json:"responses"`
		} `json:"nsd_response"`
	}
	if err := json.Unmarshal(raw, &payload); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	cb := &CallbackRecord{
		NewStatus:  payload.NewStatus,
		ReasonCode: payload.ReasonCode,
		ReasonText: payload.ReasonText,
		UpdatedAt:  payload.UpdatedAt,
	}
	if payload.NSDResp != nil {
		cb.GUID = payload.NSDResp.GUID
		cb.StatusCode = payload.NSDResp.StatusCode
		for _, rr := range payload.NSDResp.Responses {
			cb.Responses = append(cb.Responses, CallbackResponseEntry{
				ReferenceID: rr.ReferenceID, Code: rr.Code, Text: rr.Text,
			})
		}
	}
	if !s.store.ApplyCallback(gwID, cb) {
		http.Error(w, "claim not found in emulator", http.StatusNotFound)
		return
	}
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte(`{"success":true}`))
}

func (s *Server) fetchClients(ctx context.Context) ([]clientView, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.cfg.GatewayURL+"/admin/api/clients", nil)
	if err != nil {
		return nil, err
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var out []clientView
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, err
	}
	return out, nil
}

func (s *Server) renderTpl(w http.ResponseWriter, t *template.Template, data any) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := t.ExecuteTemplate(w, "layout", data); err != nil {
		log.Printf("lk-emulator: render: %v", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// randomID — генерирует короткий локальный ID заявки (8 hex).
func randomID() string {
	var b [4]byte
	_, _ = rand.Read(b[:])
	return fmt.Sprintf("%x", b)
}

func splitFio(fio string, idx int) string {
	parts := strings.Fields(fio)
	if idx < len(parts) {
		return parts[idx]
	}
	return ""
}
