package richtext

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/url"
	"strings"
	"time"
)

type PublicSessionResult struct {
	SessionResult
	DocumentURL string `json:"documentUrl,omitempty"`
	SaveURL     string `json:"saveUrl,omitempty"`
}

func (s *Service) CreatePublicSession(ctx context.Context, token, filePath, mode, password, guestID, guestName, displayName string) (*PublicSessionResult, error) {
	if !s.Cfg.Enabled {
		return nil, fmt.Errorf("rich text editor disabled")
	}
	filePath = normalizePath(filePath)
	canonical, source, importRequired, err := s.resolvePublicCanonicalPath(ctx, token, filePath, password, displayName)
	if err != nil {
		return nil, err
	}

	ownerID, ownerPath, err := s.ownerSidecarPathForPublic(ctx, token, password, canonical, displayName)
	if err != nil {
		return nil, err
	}
	roomID, err := s.resolveCollabRoomID(ctx, ownerID, ownerPath)
	if err != nil {
		return nil, err
	}
	tokenJWT, err := signRoomToken(roomTokenPayload{
		Room:    roomID,
		Path:    canonical,
		User:    "public:" + token,
		Sub:     guestID,
		Name:    guestName,
		Mode:    mode,
		Expires: time.Now().Add(8 * time.Hour).Unix(),
	}, s.Cfg.HocuspocusSecret)
	if err != nil {
		return nil, err
	}

	apiBase := strings.TrimRight(s.Cfg.APIInternalURL, "/")
	sig, _ := signPublicDocAccess(token, canonical, password, s.Cfg.HocuspocusSecret)
	docURL := fmt.Sprintf("%s/api/v1/drive/public/shares/%s/richtext/document?path=%s&password=%s&sig=%s",
		apiBase, url.PathEscape(token), url.QueryEscape(canonical), url.QueryEscape(password), url.QueryEscape(sig))
	saveURL := docURL

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

	pageSetup, _ := s.loadPublicPageSetupFromSidecar(ctx, token, password, canonical, displayName)

	return &PublicSessionResult{
		SessionResult: SessionResult{
			RoomID:         roomID,
			CanonicalPath:  canonical,
			SourcePath:     source,
			WsURL:          wsURL,
			Token:          tokenJWT,
			Mode:           mode,
			ImportRequired: importRequired,
			Collaboration:  collab,
			PageSetup:      pageSetup,
		},
		DocumentURL: docURL,
		SaveURL:     saveURL,
	}, nil
}

func (s *Service) resolvePublicCanonicalPath(ctx context.Context, token, filePath, password, displayName string) (canonical, source string, importRequired bool, err error) {
	filePath = normalizePath(filePath)
	source = filePath
	if source == "/" {
		source = s.publicClientSourcePath(ctx, token, password, filePath, displayName)
	}

	if isUltidocPath(filePath) {
		return filePath, "", false, nil
	}
	if isUltidocPath(source) {
		return source, "", false, nil
	}

	sidecar := sidecarPathForSource(source)

	if s.publicSidecarExists(ctx, token, password, sidecar, displayName) {
		needsImport := s.publicSidecarImportRequired(ctx, token, password, sidecar, source, displayName)
		return sidecar, source, needsImport, nil
	}

	if _, err := s.publicFileExists(ctx, token, source, password); err != nil {
		return "", "", false, fmt.Errorf("file not found")
	}

	return sidecar, source, true, nil
}

func (s *Service) publicSidecarImportRequired(ctx context.Context, token, password, sidecarPath, sourcePath, displayName string) bool {
	body, err := s.LoadPublicDocument(ctx, token, sidecarPath, password, displayName)
	if err != nil {
		return true
	}
	doc, err := ParseUltiDoc(body)
	if err != nil {
		return true
	}
	if !isEmptyDocContent(doc.Content) {
		return false
	}
	candidates := []string{sourcePath}
	if doc.Source != nil && doc.Source.Path != "" && doc.Source.Path != sourcePath {
		candidates = append(candidates, doc.Source.Path)
	}
	for _, src := range candidates {
		if src == "" {
			continue
		}
		exists, err := s.publicFileExists(ctx, token, src, password)
		if err == nil && exists {
			return true
		}
	}
	return false
}

func (s *Service) publicClientSourcePath(ctx context.Context, token, password, clientPath, displayName string) string {
	binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password)
	if err == nil {
		return binding.ClientSourcePath(clientPath, displayName)
	}
	if name := strings.TrimSpace(displayName); name != "" {
		return normalizePath("/" + name)
	}
	return clientPath
}

func (s *Service) publicSidecarExists(ctx context.Context, token, password, clientSidecar, displayName string) bool {
	if _, err := s.publicFileExists(ctx, token, clientSidecar, password); err == nil {
		return true
	}
	binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password)
	if err != nil {
		return false
	}
	ownerPath := binding.OwnerPathForClient(clientSidecar, displayName)
	if _, err := s.nc.FileRevision(ctx, binding.OwnerID, ownerPath); err == nil {
		return true
	}
	return false
}

func (s *Service) ImportPublicDocument(ctx context.Context, token, password, displayName string, req ImportRequest) (string, error) {
	source := normalizePath(req.SourcePath)
	canonical, _, importRequired, err := s.resolvePublicCanonicalPath(ctx, token, source, password, displayName)
	if err != nil {
		return "", err
	}
	if !importRequired {
		return canonical, nil
	}

	var content json.RawMessage
	if len(req.Content) > 0 {
		content = req.Content
	} else {
		body, err := s.LoadPublicDocument(ctx, token, source, password, displayName)
		if err != nil {
			return "", err
		}
		name := fileNameFromPath(source)
		content, err = ImportBytes(name, "", body)
		if err != nil {
			return "", err
		}
	}
	doc := NewUltiDoc(content, &UltiDocSource{
		Path:       source,
		ImportedAt: time.Now().UTC().Format(time.RFC3339),
	})
	if len(req.PageSetup) > 0 {
		var pageSetup UltiDocPageSetup
		if err := json.Unmarshal(req.PageSetup, &pageSetup); err == nil {
			doc.PageSetup = &pageSetup
		}
	}
	payload, err := doc.Marshal()
	if err != nil {
		return "", err
	}
	if err := s.SavePublicDocument(ctx, token, canonical, password, displayName, payload); err != nil {
		return "", err
	}
	return canonical, nil
}

func (s *Service) loadPublicPageSetupFromSidecar(ctx context.Context, token, password, canonicalPath, displayName string) (json.RawMessage, error) {
	body, err := s.LoadPublicDocument(ctx, token, canonicalPath, password, displayName)
	if err != nil {
		return nil, err
	}
	doc, err := ParseUltiDoc(body)
	if err != nil || doc.PageSetup == nil {
		return nil, err
	}
	return json.Marshal(doc.PageSetup)
}

func (s *Service) publicFileExists(ctx context.Context, token, path, password string) (bool, error) {
	_, err := s.nc.PublicShareFileRevision(ctx, token, path, password)
	if err != nil {
		return false, err
	}
	return true, nil
}

func (s *Service) LoadPublicDocument(ctx context.Context, token, clientPath, password, displayName string) ([]byte, error) {
	if binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password); err == nil {
		ownerPath := binding.OwnerPathForClient(clientPath, displayName)
		body, _, err := s.nc.Download(ctx, binding.OwnerID, ownerPath)
		if err == nil {
			defer body.Close()
			return io.ReadAll(body)
		}
	}
	body, _, err := s.nc.DownloadPublicShare(ctx, token, clientPath, password)
	if err != nil {
		return nil, err
	}
	defer body.Close()
	return io.ReadAll(body)
}

func (s *Service) SavePublicDocument(ctx context.Context, token, clientPath, password, displayName string, raw []byte) error {
	if binding, err := s.nc.ResolvePublicShareBinding(ctx, token, password); err == nil {
		ownerPath := binding.OwnerPathForClient(clientPath, displayName)
		reader := strings.NewReader(string(raw))
		if err := s.nc.Upload(ctx, binding.OwnerID, ownerPath, reader, "application/json"); err == nil {
			return nil
		}
	}
	reader := strings.NewReader(string(raw))
	return s.nc.UploadPublicShare(ctx, token, clientPath, password, reader, "application/json")
}

func (s *Service) LoadPublicDocumentLegacy(ctx context.Context, token, path, password string) ([]byte, error) {
	return s.LoadPublicDocument(ctx, token, path, password, "")
}

func (s *Service) SavePublicDocumentLegacy(ctx context.Context, token, path, password string, raw []byte) error {
	return s.SavePublicDocument(ctx, token, path, password, "", raw)
}
