package office

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"path"
	"strings"
	"time"

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

type Config struct {
	Enabled        bool
	DocumentURL    string // OnlyOffice Document Server API base (e.g. http://onlyoffice)
	PublicURL      string // Browser-facing Document Server URL
	APIInternalURL string // ultid base reachable from OnlyOffice container (doc fetch + callback)
	JWTSecret      string
}

type Service struct {
	nc   *nextcloud.Client
	Cfg  Config
	keys *documentKeyStore
}

func NewService(nc *nextcloud.Client, cfg Config) *Service {
	return &Service{
		nc:   nc,
		Cfg:  cfg,
		keys: newDocumentKeyStore(),
	}
}

func (s *Service) PublicURL() string {
	return strings.TrimRight(s.Cfg.PublicURL, "/")
}

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

func (s *Service) EditorConfig(ctx context.Context, ncUser, filePath, mode, editorUserID, userName string) (map[string]any, error) {
	filePath = normalizePath(filePath)
	rev, err := s.nc.FileRevision(ctx, ncUser, filePath)
	if err != nil {
		return nil, fmt.Errorf("resolve file revision: %w", err)
	}

	apiBase := strings.TrimRight(s.Cfg.APIInternalURL, "/")
	sig := ""
	if s.Cfg.JWTSecret != "" {
		sig, err = signDocAccess(ncUser, filePath, s.Cfg.JWTSecret)
		if err != nil {
			return nil, err
		}
	}
	downloadURL := buildOfficeEndpointURL(apiBase, "/api/v1/office/document", ncUser, filePath, sig)
	callbackURL := buildOfficeEndpointURL(apiBase, "/api/v1/office/callback", ncUser, filePath, "")

	return buildEditorConfig(buildEditorConfigInput{
		filePath:     filePath,
		mode:         mode,
		editorUserID: editorUserID,
		userName:     userName,
		documentKey:  s.keys.current(rev.FileID),
		downloadURL:  downloadURL,
		callbackURL:  callbackURL,
	})
}

func (s *Service) RotateDocumentKeyAfterSave(ctx context.Context, ncUser, filePath string) {
	rev, err := s.nc.FileRevision(ctx, ncUser, filePath)
	if err != nil {
		return
	}
	s.keys.rotateAfterSave(rev.FileID)
}

func (s *Service) RotatePublicDocumentKeyAfterSave(ctx context.Context, token, filePath, password string) {
	rev, err := s.nc.PublicShareFileRevision(ctx, token, filePath, password)
	if err != nil {
		return
	}
	s.keys.rotateAfterSave(rev.FileID)
}

func (s *Service) wrapEditorConfig(config map[string]any) (map[string]any, error) {
	return wrapConfig(config, s.Cfg.JWTSecret)
}

func (s *Service) OpenDocument(ctx context.Context, ncUser, filePath string) (io.ReadCloser, string, error) {
	return s.nc.Download(ctx, ncUser, normalizePath(filePath))
}

func (s *Service) SaveDocument(ctx context.Context, ncUser, filePath string, body io.Reader, contentType string) error {
	return s.nc.Upload(ctx, ncUser, normalizePath(filePath), body, contentType)
}

type buildEditorConfigInput struct {
	filePath     string
	mode         string
	editorUserID string
	userName     string
	documentKey  string
	downloadURL  string
	callbackURL  string
}

func buildEditorConfig(in buildEditorConfigInput) (map[string]any, error) {
	docType := documentType(in.filePath)
	edit := in.mode == "edit"
	document := map[string]any{
		"fileType": fileExt(in.filePath),
		"key":      in.documentKey,
		"title":    path.Base(in.filePath),
		"url":      in.downloadURL,
		"permissions": map[string]any{
			"comment":                 true,
			"copy":                    true,
			"deleteCommentAuthorOnly": false,
			"download":                true,
			"edit":                    edit,
			"editCommentAuthorOnly":   false,
			"fillForms":               edit,
			"modifyContentControl":    edit,
			"modifyFilter":            edit,
			"print":                   true,
			"review":                  true,
		},
	}
	editorCfg := map[string]any{
		"mode": in.mode,
		"user": map[string]any{
			"id":   in.editorUserID,
			"name": in.userName,
		},
		"callbackUrl": in.callbackURL,
		"coEditing": map[string]any{
			"mode":   "fast",
			"change": false,
		},
		"customization": map[string]any{
			"autosave":  true,
			"forcesave": true,
		},
	}
	config := map[string]any{
		"documentType": docType,
		"document":     document,
		"editorConfig": editorCfg,
	}
	return config, nil
}

func documentType(filePath string) string {
	ext := strings.ToLower(path.Ext(filePath))
	switch ext {
	case ".xlsx", ".xls", ".xlsb", ".xlsm", ".xlt", ".xltm", ".xltx",
		".ods", ".ots", ".csv", ".tsv", ".fods", ".et", ".ett", ".sxc":
		return "cell"
	case ".pptx", ".ppt", ".pptm", ".pot", ".potm", ".potx",
		".pps", ".ppsm", ".ppsx", ".odp", ".otp", ".odg", ".fodp",
		".dps", ".dpt", ".sxi":
		return "slide"
	case ".vsdm", ".vsdx", ".vssm", ".vssx", ".vstm", ".vstx":
		return "diagram"
	default:
		return "word"
	}
}

func fileExt(filePath string) string {
	return strings.TrimPrefix(strings.ToLower(path.Ext(filePath)), ".")
}

func normalizePath(p string) string {
	p = strings.TrimSpace(p)
	if p == "" {
		return "/"
	}
	if !strings.HasPrefix(p, "/") {
		p = "/" + p
	}
	return p
}

func buildOfficeEndpointURL(base, endpoint, ncUser, filePath, sig string) string {
	q := url.Values{}
	q.Set("path", normalizePath(filePath))
	q.Set("user", ncUser)
	if sig != "" {
		q.Set("sig", sig)
	}
	return strings.TrimRight(base, "/") + endpoint + "?" + q.Encode()
}

func signDocAccess(ncUser, filePath, secret string) (string, error) {
	payload := map[string]any{
		"user": ncUser,
		"path": normalizePath(filePath),
		"exp":  time.Now().Add(2 * time.Hour).Unix(),
	}
	return signJWT(payload, secret)
}

func VerifyDocAccess(ncUser, filePath, sig, secret string) bool {
	payload, err := verifyJWT(sig, secret)
	if err != nil {
		return false
	}
	if payload["user"] != ncUser || payload["path"] != normalizePath(filePath) {
		return false
	}
	if exp, ok := payload["exp"].(float64); ok && int64(exp) < time.Now().Unix() {
		return false
	}
	return true
}
