package topdata

import (
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"gitea.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
)

const (
	defaultDeployManifestName = ".wiki_deploy_manifest.json"
	defaultEditSummary        = "Auto-generated from native builder"
	defaultNotesDelimiter     = "===== Notes ====="
)

var defaultManagedNamespaces = []string{"classes", "feat", "itemtypes", "races", "skills", "spells"}

type DeployWikiOptions struct {
	SourceDir      string
	Endpoint       string
	Username       string
	Password       string
	Version        string
	Namespaces     []string
	NotesDelimiter string
	ManifestPath   string
	DryRun         bool
}

type deployResult struct {
	LocalPages int
	Updated    int
	Deleted    int
	Skipped    int
	Manifest   string
}

type deployPlan struct {
	PageID     string
	Content    string
	OldContent string
}

type manifestEntry struct {
	Hash string `json:"hash"`
}

func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress func(string)) (deployResult, error) {
	if progress == nil {
		progress = func(string) {}
	}

	if opts.SourceDir == "" {
		opts.SourceDir = wikiOutputPagesDir(p)
	}
	if _, err := os.Stat(opts.SourceDir); err != nil {
		return deployResult{}, fmt.Errorf("wiki source directory not found: %w", err)
	}

	if opts.Endpoint == "" {
		return deployResult{}, errors.New("DOKUWIKI_RPC_ENDPOINT is required")
	}
	if opts.Username == "" {
		return deployResult{}, errors.New("DOKUWIKI_RPC_USERNAME is required")
	}
	if opts.Password == "" {
		return deployResult{}, errors.New("DOKUWIKI_RPC_PASSWORD is required")
	}

	namespaces := opts.Namespaces
	if len(namespaces) == 0 {
		namespaces = defaultManagedNamespaces
	}

	notesDelimiter := opts.NotesDelimiter
	if notesDelimiter == "" {
		notesDelimiter = defaultNotesDelimiter
	}

	manifestPath := opts.ManifestPath
	if manifestPath == "" {
		manifestPath = filepath.Join(filepath.Dir(opts.SourceDir), defaultDeployManifestName)
	}

	localPages, err := collectLocalPages(opts.SourceDir, namespaces)
	if err != nil {
		return deployResult{}, err
	}

	manifest := loadDeployManifest(manifestPath)
	client := newDokuWikiClient(opts.Endpoint, opts.Username, opts.Password)

	deployments, stalePages, err := planDeploy(localPages, client, namespaces, notesDelimiter, manifest)
	if err != nil {
		return deployResult{}, err
	}

	progress(fmt.Sprintf("Deploying %d pages...", len(deployments)))

	editSummary := defaultEditSummary
	if opts.Version != "" {
		editSummary = fmt.Sprintf("%s (%s)", defaultEditSummary, opts.Version)
	}

	if !opts.DryRun {
		for _, plan := range deployments {
			if err := client.putPage(plan.PageID, plan.Content, editSummary); err != nil {
				return deployResult{}, err
			}
		}
		for _, pageID := range stalePages {
			if err := client.deletePage(pageID, editSummary); err != nil {
				return deployResult{}, err
			}
		}

		currentHashes := computePageHashes(localPages, notesDelimiter)
		saveDeployManifest(manifestPath, currentHashes)
	}

	updated := len(deployments)
	deleted := len(stalePages)
	skipped := len(localPages) - updated

	result := deployResult{
		LocalPages: len(localPages),
		Updated:    updated,
		Deleted:    deleted,
		Skipped:    skipped,
		Manifest:   manifestPath,
	}

	progress(fmt.Sprintf("Updated: %d, Skipped: %d, Deleted: %d", updated, skipped, deleted))

	return result, nil
}

func collectLocalPages(sourceDir string, namespaces []string) (map[string]string, error) {
	pages := map[string]string{}
	for _, ns := range namespaces {
		nsDir := filepath.Join(sourceDir, ns)
		if !isDir(nsDir) {
			continue
		}
		if err := filepath.WalkDir(nsDir, func(path string, d os.DirEntry, err error) error {
			if err != nil {
				return err
			}
			if d.IsDir() {
				return nil
			}
			if filepath.Ext(path) != ".txt" {
				return nil
			}
			rel, err := filepath.Rel(sourceDir, path)
			if err != nil {
				return err
			}
			pageID := strings.TrimSuffix(rel, ".txt")
			pageID = strings.ReplaceAll(pageID, string(filepath.Separator), ":")
			pageID = strings.ToLower(pageID)
			content, err := os.ReadFile(path)
			if err != nil {
				return err
			}
			pages[pageID] = string(content)
			return nil
		}); err != nil {
			return nil, err
		}
	}
	return pages, nil
}

func computePageHash(content, notesDelimiter string) string {
	generatedBody, _ := splitNotesSection(content, notesDelimiter)
	h := sha1.New()
	h.Write([]byte(generatedBody))
	return fmt.Sprintf("%x", h.Sum(nil))
}

func computePageHashes(pages map[string]string, notesDelimiter string) map[string]string {
	hashes := map[string]string{}
	for pageID, content := range pages {
		hashes[pageID] = computePageHash(content, notesDelimiter)
	}
	return hashes
}

func splitNotesSection(content, notesDelimiter string) (string, string) {
	lines := strings.Split(content, notesDelimiter)
	if len(lines) <= 1 {
		return strings.TrimSpace(content), ""
	}
	body := strings.TrimSpace(strings.Join(lines[:len(lines)-1], notesDelimiter))
	notes := strings.TrimSpace(strings.Join(lines[1:], notesDelimiter))
	return body, notes
}

func loadDeployManifest(path string) map[string]string {
	if _, err := os.Stat(path); err != nil {
		return map[string]string{}
	}
	data, err := os.ReadFile(path)
	if err != nil {
		return map[string]string{}
	}
	var doc struct {
		Pages map[string]string `json:"pages"`
	}
	if err := json.Unmarshal(data, &doc); err != nil {
		return map[string]string{}
	}
	result := map[string]string{}
	for k, v := range doc.Pages {
		result[k] = v
	}
	return result
}

func saveDeployManifest(path string, hashes map[string]string) {
	os.MkdirAll(filepath.Dir(path), 0755)
	doc := struct {
		Pages map[string]string `json:"pages"`
	}{
		Pages: hashes,
	}
	data, _ := json.MarshalIndent(doc, "", " ")
	os.WriteFile(path, append(data, '\n'), 0644)
}

func planDeploy(
	localPages map[string]string,
	client *dokuWikiClient,
	namespaces []string,
	notesDelimiter string,
	previousHashes map[string]string,
) ([]deployPlan, []string, error) {
	var deployments []deployPlan
	remotePages := map[string]string{}

	for pageID, content := range localPages {
		hash := computePageHash(content, notesDelimiter)
		if previousHashes[pageID] == hash {
			continue
		}

		oldContent, err := client.getPage(pageID)
		if err != nil {
			if !errors.Is(err, errPageNotFound) {
				return nil, nil, err
			}
			oldContent = ""
		}
		remotePages[pageID] = oldContent

		merged := mergePageContent(content, oldContent, notesDelimiter)
		if merged != oldContent {
			deployments = append(deployments, deployPlan{
				PageID:     pageID,
				Content:    merged,
				OldContent: oldContent,
			})
		}
	}

	remoteIDs := map[string]bool{}
	for pageID := range remotePages {
		remoteIDs[pageID] = true
	}

	for _, ns := range namespaces {
		pages, err := client.listNamespacePages(ns)
		if err != nil {
			continue
		}
		for _, pageID := range pages {
			remoteIDs[pageID] = true
		}
	}

	var stale []string
	for pageID := range remoteIDs {
		if _, exists := localPages[pageID]; !exists {
			stale = append(stale, pageID)
		}
	}
	slices.Sort(stale)

	return deployments, stale, nil
}

func mergePageContent(generated, existing, notesDelimiter string) string {
	_, existingNotes := splitNotesSection(existing, notesDelimiter)
	generatedBody, _ := splitNotesSection(generated, notesDelimiter)

	if existingNotes == "" {
		return generated
	}

	return strings.TrimSpace(generatedBody) + "\n\n" + notesDelimiter + "\n\n" + existingNotes + "\n"
}

type dokuWikiClient struct {
	endpoint      string
	username      string
	password      string
	timeout       time.Duration
	retryAttempts int
	retryBackoff  time.Duration
	requestDelay  time.Duration
}

var errPageNotFound = errors.New("page not found")

func newDokuWikiClient(endpoint, username, password string) *dokuWikiClient {
	endpoint = strings.TrimRight(endpoint, "/")
	if strings.HasSuffix(endpoint, "/doku.php") {
		endpoint = strings.TrimSuffix(endpoint, "/doku.php") + "/lib/exe/jsonrpc.php"
	}
	return &dokuWikiClient{
		endpoint:      endpoint,
		username:      username,
		password:      password,
		timeout:       30 * time.Second,
		retryAttempts: 3,
		retryBackoff:  500 * time.Millisecond,
		requestDelay:  50 * time.Millisecond,
	}
}

func (c *dokuWikiClient) getPage(pageID string) (string, error) {
	result, err := c.rpc("wiki.getPage", []any{pageID})
	if err != nil {
		return "", err
	}
	if result == nil {
		return "", errPageNotFound
	}
	s, ok := result.(string)
	if !ok {
		return "", nil
	}
	return s, nil
}

func (c *dokuWikiClient) putPage(pageID, content, summary string) error {
	_, err := c.rpc("wiki.putPage", []any{pageID, content, map[string]any{"sum": summary}})
	return err
}

func (c *dokuWikiClient) deletePage(pageID, summary string) error {
	_, err := c.rpc("wiki.putPage", []any{pageID, "", map[string]any{"sum": summary, "minor": false}})
	return err
}

func (c *dokuWikiClient) listNamespacePages(namespace string) ([]string, error) {
	result, err := c.rpc("dokuwiki.getPagelist", []any{namespace, map[string]any{}})
	if err != nil {
		return nil, err
	}
	items, ok := result.([]any)
	if !ok {
		return nil, nil
	}
	var pages []string
	for _, item := range items {
		switch v := item.(type) {
		case string:
			pages = append(pages, v)
		case map[string]any:
			if id, ok := v["id"].(string); ok {
				pages = append(pages, id)
			}
		}
	}
	return pages, nil
}

func (c *dokuWikiClient) rpc(method string, params []any) (any, error) {
	var last error
	for attempt := 1; attempt <= c.retryAttempts; attempt++ {
		result, err := c.rpcOnce(method, params)
		if err == nil {
			return result, nil
		}
		last = err
		if attempt < c.retryAttempts {
			time.Sleep(c.retryBackoff * time.Duration(attempt))
		}
	}
	return nil, fmt.Errorf("%s failed after %d attempts: %w", method, c.retryAttempts, last)
}

func (c *dokuWikiClient) rpcOnce(method string, params []any) (any, error) {
	if c.requestDelay > 0 {
		time.Sleep(c.requestDelay)
	}

	payload := map[string]any{
		"jsonrpc": "2.0",
		"method":  method,
		"params":  params,
		"id":      1,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	token := base64.StdEncoding.EncodeToString([]byte(c.username + ":" + c.password))
	req, err := http.NewRequest("POST", c.endpoint, strings.NewReader(string(body)))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Basic "+token)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	var result map[string]any
	if err := json.Unmarshal(data, &result); err != nil {
		preview := strings.TrimSpace(string(data))
		if len(preview) > 200 {
			preview = preview[:200] + "..."
		}
		return nil, fmt.Errorf("non-JSON response: %s", preview)
	}

	if errMsg, ok := result["error"].(any); ok && errMsg != nil {
		return nil, fmt.Errorf("RPC error: %v", errMsg)
	}

	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	return result["result"], nil
}

func isDir(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.IsDir()
}
