package richtext

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"strings"
	"time"

	"github.com/ultisuite/ulti-backend/internal/auth"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
)

type Service struct {
	nc   *nextcloud.Client
	Cfg  Config
	hub  fileChangePublisher
}

type fileChangePublisher interface {
	PublishFileChanged(platformUserID, path string)
}

func NewService(nc *nextcloud.Client, cfg Config, hub fileChangePublisher) *Service {
	if cfg.StorageMode == "" {
		cfg.StorageMode = "sidecar"
	}
	return &Service{nc: nc, Cfg: cfg, hub: hub}
}

func (s *Service) EnsureNextcloudUser(ctx context.Context, claims *auth.Claims) (string, error) {
	return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
}

type SessionResult struct {
	RoomID         string `json:"roomId"`
	CanonicalPath  string `json:"canonicalPath"`
	SourcePath     string `json:"sourcePath,omitempty"`
	WsURL          string `json:"wsUrl"`
	Token          string `json:"token"`
	Mode           string `json:"mode"`
	ImportRequired bool   `json:"importRequired"`
	Collaboration  bool   `json:"collaboration"`
}

func (s *Service) CreateSession(ctx context.Context, ncUser, filePath, mode, editorUserID, editorName string) (*SessionResult, error) {
	if !s.Cfg.Enabled {
		return nil, fmt.Errorf("rich text editor disabled")
	}
	filePath = normalizePath(filePath)
	if mode == "" {
		mode = "edit"
	}

	canonical, source, importRequired, err := s.resolveCanonicalPath(ctx, ncUser, filePath)
	if err != nil {
		return nil, err
	}

	roomID, err := s.resolveCollabRoomID(ctx, ncUser, canonical)
	if err != nil {
		return nil, err
	}

	token, err := signRoomToken(roomTokenPayload{
		Room:     roomID,
		Path:     canonical,
		User:     ncUser,
		Sub:      editorUserID,
		Name:     editorName,
		Mode:     mode,
		Expires:  time.Now().Add(8 * time.Hour).Unix(),
	}, s.Cfg.HocuspocusSecret)
	if err != nil {
		return nil, err
	}

	wsURL := strings.TrimSpace(s.Cfg.HocuspocusPublicURL)
	collab := wsURL != "" && s.Cfg.HocuspocusSecret != ""

	return &SessionResult{
		RoomID:         roomID,
		CanonicalPath:  canonical,
		SourcePath:     source,
		WsURL:          wsURL,
		Token:          token,
		Mode:           mode,
		ImportRequired: importRequired,
		Collaboration:  collab,
	}, nil
}

func (s *Service) resolveCanonicalPath(ctx context.Context, ncUser, filePath string) (canonical, source string, importRequired bool, err error) {
	if isUltidocPath(filePath) {
		return filePath, "", false, nil
	}

	sidecar := sidecarPathForSource(filePath)
	if s.Cfg.StorageMode == "overwrite" {
		target := sidecarPathForSource(filePath)
		if _, err := s.nc.FileRevision(ctx, ncUser, target); err == nil {
			return target, filePath, false, nil
		}
		// Will import and optionally move — treat as import required if source exists.
		if _, err := s.nc.FileRevision(ctx, ncUser, filePath); err == nil {
			return target, filePath, true, nil
		}
		return "", "", false, fmt.Errorf("file not found: %s", filePath)
	}

	if _, err := s.nc.FileRevision(ctx, ncUser, sidecar); err == nil {
		return sidecar, filePath, false, nil
	}
	if _, err := s.nc.FileRevision(ctx, ncUser, filePath); err != nil {
		return "", "", false, fmt.Errorf("file not found: %s", filePath)
	}
	return sidecar, filePath, true, nil
}

func (s *Service) LoadDocument(ctx context.Context, ncUser, path string) ([]byte, error) {
	path = normalizePath(path)
	body, _, err := s.nc.Download(ctx, ncUser, path)
	if err != nil {
		return nil, err
	}
	defer body.Close()
	return io.ReadAll(body)
}

func (s *Service) SaveDocument(ctx context.Context, ncUser, path string, raw []byte, platformUserID string) error {
	path = normalizePath(path)
	reader := strings.NewReader(string(raw))
	if strings.HasPrefix(ncUser, "public:") {
		token := strings.TrimPrefix(ncUser, "public:")
		return s.SavePublicDocumentLegacy(ctx, token, path, "", raw)
	}
	if err := s.nc.Upload(ctx, ncUser, path, reader, "application/json"); err != nil {
		return err
	}
	if s.hub != nil && platformUserID != "" {
		s.hub.PublishFileChanged(platformUserID, path)
	}
	return nil
}

func (s *Service) LoadDocumentForUser(ctx context.Context, ncUser, path string) ([]byte, error) {
	path = normalizePath(path)
	if strings.HasPrefix(ncUser, "public:") {
		token := strings.TrimPrefix(ncUser, "public:")
		return s.LoadPublicDocumentLegacy(ctx, token, path, "")
	}
	return s.LoadDocument(ctx, ncUser, path)
}

type ImportRequest struct {
	SourcePath string          `json:"source_path"`
	Content    json.RawMessage `json:"content,omitempty"`
	YjsState   string          `json:"yjsState,omitempty"`
}

func (s *Service) ImportDocument(ctx context.Context, ncUser, platformUserID string, req ImportRequest) (string, error) {
	source := normalizePath(req.SourcePath)
	canonical, _, importRequired, err := s.resolveCanonicalPath(ctx, ncUser, source)
	if err != nil {
		return "", err
	}
	if !importRequired && len(req.Content) == 0 {
		return canonical, nil
	}

	var content json.RawMessage
	if len(req.Content) > 0 {
		content = req.Content
	} else {
		body, _, err := s.nc.Download(ctx, ncUser, source)
		if err != nil {
			return "", err
		}
		raw, err := io.ReadAll(body)
		body.Close()
		if err != nil {
			return "", err
		}
		name := fileNameFromPath(source)
		mime := ""
		content, err = ImportBytes(name, mime, raw)
		if err != nil {
			return "", err
		}
	}

	doc := NewUltiDoc(content, &UltiDocSource{
		Path:       source,
		ImportedAt: time.Now().UTC().Format(time.RFC3339),
	})
	if req.YjsState != "" {
		doc.YjsState = req.YjsState
	}
	payload, err := doc.Marshal()
	if err != nil {
		return "", err
	}

	if err := s.SaveDocument(ctx, ncUser, canonical, payload, platformUserID); err != nil {
		return "", err
	}

	if s.Cfg.StorageMode == "overwrite" && source != canonical {
		if err := s.nc.Move(ctx, ncUser, source, canonical); err != nil {
			return canonical, nil
		}
	}

	if source != canonical {
		if err := replicateFileShares(ctx, s.nc, ncUser, source, canonical); err != nil {
			return canonical, fmt.Errorf("save ok but share replication failed: %w", err)
		}
	}

	return canonical, nil
}

func replicateFileShares(ctx context.Context, nc *nextcloud.Client, userID, fromPath, toPath string) error {
	shares, err := nc.ListShares(ctx, userID, fromPath)
	if err != nil {
		return err
	}
	for _, sh := range shares {
		opts := nextcloud.CreateShareOptions{
			ShareType:   sh.ShareType,
			Permissions: sh.Permissions,
			ShareWith:   sh.ShareWith,
			ExpireDate:  sh.ExpiresAt,
			Note:        sh.Note,
			Label:       sh.Label,
		}
		if _, err := nc.CreateShare(ctx, userID, toPath, opts); err != nil {
			return err
		}
	}
	return nil
}

type ExportRequest struct {
	Path   string `json:"path"`
	Format string `json:"format"`
}

func (s *Service) ExportDocument(ctx context.Context, ncUser, path, format string) ([]byte, string, error) {
	path = normalizePath(path)
	raw, err := s.LoadDocument(ctx, ncUser, path)
	if err != nil {
		return nil, "", err
	}
	doc, err := ParseUltiDoc(raw)
	if err != nil {
		return nil, "", err
	}
	format = strings.ToLower(strings.TrimSpace(format))
	switch format {
	case "json", "ultidoc":
		out, err := json.MarshalIndent(doc, "", "  ")
		return out, "application/json", err
	case "txt", "text":
		return []byte(contentToPlainText(doc.Content)), "text/plain; charset=utf-8", nil
	case "html", "htm":
		return []byte(contentToHTML(doc.Content)), "text/html; charset=utf-8", nil
	default:
		return nil, "", fmt.Errorf("unsupported export format %q", format)
	}
}

func contentToPlainText(content json.RawMessage) string {
	var node map[string]any
	if err := json.Unmarshal(content, &node); err != nil {
		return ""
	}
	var b strings.Builder
	extractText(node, &b)
	return strings.TrimSpace(b.String())
}

func extractText(node map[string]any, b *strings.Builder) {
	if t, ok := node["text"].(string); ok {
		b.WriteString(t)
	}
	children, _ := node["content"].([]any)
	for _, ch := range children {
		if m, ok := ch.(map[string]any); ok {
			extractText(m, b)
		}
	}
	if typ, _ := node["type"].(string); typ == "paragraph" || typ == "heading" {
		b.WriteString("\n")
	}
}

func contentToHTML(content json.RawMessage) string {
	text := contentToPlainText(content)
	if text == "" {
		return "<p></p>"
	}
	var b strings.Builder
	b.WriteString("<html><body>")
	for _, line := range strings.Split(text, "\n") {
		b.WriteString("<p>")
		b.WriteString(htmlEscape(line))
		b.WriteString("</p>")
	}
	b.WriteString("</body></html>")
	return b.String()
}

func htmlEscape(s string) string {
	r := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;")
	return r.Replace(s)
}

func hashPath(p string) string {
	h := sha256Hex([]byte(normalizePath(p)))
	if len(h) > 16 {
		return h[:16]
	}
	return h
}
