package nextcloud

import (
	"context"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

// PublicShareView describes a public link share (file or folder).
type PublicShareView struct {
	Token              string     `json:"token"`
	Name               string     `json:"name"`
	ItemType           string     `json:"item_type"`
	Path               string     `json:"path"`
	Permissions        int        `json:"permissions"`
	OwnerID            string     `json:"owner_id,omitempty"`
	OwnerDisplayName   string     `json:"owner_displayname,omitempty"`
	Files              []FileInfo `json:"files,omitempty"`
	File               *FileInfo  `json:"file,omitempty"`
}

func (c *Client) PublicShareURL(token string) string {
	token = strings.TrimSpace(token)
	if token == "" || c.drivePublicURL == "" {
		return ""
	}
	return strings.TrimRight(c.drivePublicURL, "/") + "/s/" + url.PathEscape(token)
}

func (c *Client) WithDrivePublicURL(publicURL string) *Client {
	if c == nil {
		return nil
	}
	c.drivePublicURL = strings.TrimRight(strings.TrimSpace(publicURL), "/")
	return c
}

func publicShareDAVPath(token, relPath string) string {
	token = strings.TrimSpace(token)
	relPath = NormalizeClientPath(relPath)
	base := "/public.php/dav/files/" + url.PathEscape(token)
	if relPath == "/" {
		return base + "/"
	}
	trimmed := strings.Trim(relPath, "/")
	parts := strings.Split(trimmed, "/")
	for i, p := range parts {
		parts[i] = url.PathEscape(p)
	}
	return base + "/" + strings.Join(parts, "/")
}

func clientPathFromPublicShareHref(href, token string) string {
	href = strings.TrimSpace(href)
	marker := "/public.php/dav/files/" + url.PathEscape(token)
	if idx := strings.Index(href, marker); idx >= 0 {
		rest := strings.TrimPrefix(href[idx+len(marker):], "/")
		if rest == "" {
			return "/"
		}
		return decodeDAVPath("/" + rest)
	}
	// Fallback: strip through token segment in path.
	parts := strings.Split(strings.Trim(href, "/"), "/")
	for i, p := range parts {
		if decodeDAVSegment(p) == token && i+1 < len(parts) {
			return decodeDAVPath("/" + strings.Join(parts[i+1:], "/"))
		}
	}
	return "/"
}

func (c *Client) GetPublicShare(ctx context.Context, token, relPath, password string) (*PublicShareView, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return nil, ErrInvalidPublicShare
	}
	relPath = NormalizeClientPath(relPath)

	resp, err := c.publicShareRequest(ctx, "PROPFIND", token, relPath, strings.NewReader(propfindListBody), password, map[string]string{
		"Depth":        "1",
		"Content-Type": "application/xml",
	})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return nil, ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
		return nil, &HTTPStatusError{Operation: "public share propfind", StatusCode: resp.StatusCode}
	}

	root, children, permissions, err := parsePublicSharePropfind(resp.Body, token, relPath)
	if err != nil {
		return nil, err
	}

	view := &PublicShareView{
		Token:       token,
		Path:        relPath,
		Name:        root.Name,
		Permissions: permissions,
	}
	if sharePerms, permErr := c.GetPublicSharePermissions(ctx, token, password); permErr == nil {
		view.Permissions = sharePerms
	}
	if root.Type == "directory" {
		view.ItemType = "folder"
		view.Files = children
	} else {
		view.ItemType = "file"
		view.File = &root
	}
	c.enrichPublicShareOwner(ctx, view, token, password)
	return view, nil
}

// PreviewPublicShare returns a thumbnail/preview image for a file in a public link share.
func (c *Client) PreviewPublicShare(ctx context.Context, token, filePath, password string, width, height int) (io.ReadCloser, string, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return nil, "", ErrInvalidPublicShare
	}
	filePath = NormalizeClientPath(filePath)
	if width <= 0 {
		width = 400
	}
	if height <= 0 {
		height = 300
	}
	if width > 2048 {
		width = 2048
	}
	if height > 2048 {
		height = 2048
	}

	q := url.Values{}
	q.Set("file", filePath)
	q.Set("x", strconv.Itoa(width))
	q.Set("y", strconv.Itoa(height))
	q.Set("a", "1")
	previewPath := fmt.Sprintf(
		"/index.php/apps/files_sharing/publicpreview/%s?%s",
		url.PathEscape(token),
		q.Encode(),
	)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+previewPath, nil)
	if err != nil {
		return nil, "", err
	}
	req.Header.Set("X-Requested-With", "XMLHttpRequest")
	if password != "" {
		req.SetBasicAuth("", password)
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, "", err
	}
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		resp.Body.Close()
		return nil, "", ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, "", &HTTPStatusError{Operation: "public share preview", StatusCode: resp.StatusCode}
	}
	contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
	if contentType == "" {
		contentType = "image/jpeg"
	}
	return resp.Body, contentType, nil
}

func (c *Client) DownloadPublicShare(ctx context.Context, token, filePath, password string) (io.ReadCloser, string, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return nil, "", ErrInvalidPublicShare
	}
	filePath = NormalizeClientPath(filePath)

	body, contentType, err := c.downloadPublicShareAt(ctx, token, filePath, password)
	if err == nil {
		return body, contentType, nil
	}
	// Single-file shares expose content at the WebDAV root, not /filename.
	var statusErr *HTTPStatusError
	if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusNotFound && filePath != "/" {
		return c.downloadPublicShareAt(ctx, token, "/", password)
	}
	return nil, "", err
}

func (c *Client) downloadPublicShareAt(ctx context.Context, token, filePath, password string) (io.ReadCloser, string, error) {
	davPath := publicShareDAVPath(token, filePath)
	resp, err := c.publicShareRequestRaw(ctx, http.MethodGet, davPath, nil, password, nil)
	if err != nil {
		return nil, "", err
	}
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		resp.Body.Close()
		return nil, "", ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, "", &HTTPStatusError{Operation: "public share download", StatusCode: resp.StatusCode}
	}
	contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	return resp.Body, contentType, nil
}

func (c *Client) publicShareRequest(ctx context.Context, method, token, relPath string, body io.Reader, password string, headers map[string]string) (*http.Response, error) {
	return c.publicShareRequestRaw(ctx, method, publicShareDAVPath(token, relPath), body, password, headers)
}

func (c *Client) publicShareRequestRaw(ctx context.Context, method, davPath string, body io.Reader, password string, headers map[string]string) (*http.Response, error) {
	if !strings.HasPrefix(davPath, "/") {
		davPath = "/" + davPath
	}
	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+davPath, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("X-Requested-With", "XMLHttpRequest")
	if password != "" {
		req.SetBasicAuth("", password)
	}
	for k, v := range headers {
		req.Header.Set(k, v)
	}
	return c.httpClient.Do(req)
}

const propfindListBody = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  <d:prop>
    <d:getlastmodified/>
    <d:getcontenttype/>
    <d:getcontentlength/>
    <d:resourcetype/>
    <d:getetag/>
    <d:displayname/>
    <oc:size/>
    <oc:permissions/>
  </d:prop>
</d:propfind>`

const propfindOwnerBody = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  <d:prop>
    <oc:owner-id/>
  </d:prop>
</d:propfind>`

func (c *Client) enrichPublicShareOwner(ctx context.Context, view *PublicShareView, token, password string) {
	if c == nil || view == nil {
		return
	}
	ownerID, err := c.getPublicShareOwnerID(ctx, token, password)
	if err != nil || strings.TrimSpace(ownerID) == "" {
		return
	}
	view.OwnerID = ownerID
	if name, err := c.UserDisplayName(ctx, ownerID); err == nil && strings.TrimSpace(name) != "" {
		view.OwnerDisplayName = strings.TrimSpace(name)
	}
}

func (c *Client) getPublicShareOwnerID(ctx context.Context, token, password string) (string, error) {
	resp, err := c.publicShareRequest(ctx, "PROPFIND", token, "/", strings.NewReader(propfindOwnerBody), password, map[string]string{
		"Depth":        "0",
		"Content-Type": "application/xml",
	})
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return "", ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
		return "", &HTTPStatusError{Operation: "public share owner", StatusCode: resp.StatusCode}
	}
	var ms multistatus
	if err := xml.NewDecoder(resp.Body).Decode(&ms); err != nil {
		return "", err
	}
	if len(ms.Responses) == 0 {
		return "", nil
	}
	return strings.TrimSpace(ms.Responses[0].Propstat.Prop.OwnerID), nil
}

const propfindPermBody = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  <d:prop>
    <oc:permissions/>
  </d:prop>
</d:propfind>`

func (c *Client) GetPublicSharePermissions(ctx context.Context, token, password string) (int, error) {
	return c.GetPublicSharePathPermissions(ctx, token, "/", password)
}

// EffectivePublicSharePermissions returns share permissions for a path.
// Nextcloud often omits oc:permissions on nested WebDAV nodes; fall back to root share bits.
func (c *Client) EffectivePublicSharePermissions(ctx context.Context, token, relPath, password string) (int, error) {
	root, err := c.GetPublicSharePermissions(ctx, token, password)
	if err != nil {
		return 0, err
	}
	relPath = NormalizeClientPath(relPath)
	if relPath == "/" {
		return root, nil
	}
	pathPerms, err := c.GetPublicSharePathPermissions(ctx, token, relPath, password)
	if err != nil || pathPerms == 0 {
		return root, nil
	}
	return root | pathPerms, nil
}

const propfindPublicRevisionBody = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  <d:prop>
    <oc:fileid/>
    <d:getetag/>
  </d:prop>
</d:propfind>`

func (c *Client) PublicShareFileRevision(ctx context.Context, token, filePath, password string) (FileRevision, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return FileRevision{}, ErrInvalidPublicShare
	}
	filePath = NormalizeClientPath(filePath)

	resp, err := c.publicShareRequest(ctx, "PROPFIND", token, filePath, strings.NewReader(propfindPublicRevisionBody), password, map[string]string{
		"Depth":        "0",
		"Content-Type": "application/xml",
	})
	if err != nil {
		return FileRevision{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return FileRevision{}, ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
		return FileRevision{}, &HTTPStatusError{Operation: "public share file revision", StatusCode: resp.StatusCode}
	}

	var ms multistatus
	if err := xml.NewDecoder(resp.Body).Decode(&ms); err != nil {
		return FileRevision{}, err
	}
	if len(ms.Responses) == 0 {
		return FileRevision{}, fmt.Errorf("public share file revision: empty response")
	}
	raw := strings.TrimSpace(ms.Responses[0].Propstat.Prop.FileID)
	if raw == "" {
		return FileRevision{}, fmt.Errorf("public share file revision: missing fileid")
	}
	id, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return FileRevision{}, fmt.Errorf("public share file revision: invalid fileid %q", raw)
	}
	etag := strings.Trim(strings.TrimSpace(ms.Responses[0].Propstat.Prop.ETag), "\"")
	return FileRevision{FileID: id, ETag: etag}, nil
}

func (c *Client) GetPublicSharePathPermissions(ctx context.Context, token, relPath, password string) (int, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return 0, ErrInvalidPublicShare
	}
	relPath = NormalizeClientPath(relPath)

	resp, err := c.publicShareRequest(ctx, "PROPFIND", token, relPath, strings.NewReader(propfindPermBody), password, map[string]string{
		"Depth":        "0",
		"Content-Type": "application/xml",
	})
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return 0, ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusMultiStatus && resp.StatusCode != http.StatusOK {
		return 0, &HTTPStatusError{Operation: "public share permissions", StatusCode: resp.StatusCode}
	}
	var ms multistatus
	if err := xml.NewDecoder(resp.Body).Decode(&ms); err != nil {
		return 0, err
	}
	if len(ms.Responses) == 0 {
		return 0, fmt.Errorf("public share: empty permissions response")
	}
	return ParseOCPermissionLetters(ms.Responses[0].Propstat.Prop.Permissions), nil
}

func parsePublicSharePropfind(body io.Reader, token, listDir string) (FileInfo, []FileInfo, int, error) {
	var ms multistatus
	if err := xml.NewDecoder(body).Decode(&ms); err != nil {
		return FileInfo{}, nil, 0, err
	}
	if len(ms.Responses) == 0 {
		return FileInfo{}, nil, 0, fmt.Errorf("public share: empty response")
	}

	root := fileInfoFromPublicDAV(ms.Responses[0], token, listDir)
	permissions := ParseOCPermissionLetters(ms.Responses[0].Propstat.Prop.Permissions)
	children := make([]FileInfo, 0, len(ms.Responses)-1)
	for i := 1; i < len(ms.Responses); i++ {
		child := fileInfoFromPublicDAV(ms.Responses[i], token, listDir)
		if child.Name == "" {
			continue
		}
		children = append(children, child)
	}
	return root, children, permissions, nil
}

func fileInfoFromPublicDAV(r response, token, listDir string) FileInfo {
	name := fileNameFromDAVProp(r.Propstat.Prop.DisplayName, r.Href)
	clientPath := clientPathFromPublicShareHref(r.Href, token)
	if name == "" {
		name = pathBaseName(strings.Trim(clientPath, "/"))
	}
	if clientPath == "/" && name == "" {
		name = "Partage"
	}

	fileType := "file"
	if r.Propstat.Prop.ResourceType.Collection != nil {
		fileType = "directory"
	}

	size := r.Propstat.Prop.ContentLength
	if r.Propstat.Prop.Size > 0 {
		size = r.Propstat.Prop.Size
	}

	displayPath := clientPath
	if displayPath == "/" && fileType == "file" {
		// Single-file shares: WebDAV root is the file itself.
		displayPath = "/"
	} else if displayPath == "/" {
		displayPath = "/" + name
	}

	return FileInfo{
		Path:         displayPath,
		Name:         name,
		Type:         fileType,
		Size:         size,
		MimeType:     r.Propstat.Prop.ContentType,
		LastModified: r.Propstat.Prop.LastModified,
		ETag:         strings.Trim(r.Propstat.Prop.ETag, "\""),
	}
}

var (
	ErrInvalidPublicShare          = errors.New("invalid public share token")
	ErrPublicSharePasswordRequired = errors.New("public share password required")
	ErrPublicShareReadOnly         = errors.New("public share read only")
)

func (c *Client) UploadPublicShare(ctx context.Context, token, filePath, password string, content io.Reader, contentType string) error {
	token = strings.TrimSpace(token)
	if token == "" {
		return ErrInvalidPublicShare
	}
	filePath = NormalizeClientPath(filePath)
	headers := map[string]string{}
	if contentType != "" {
		headers["Content-Type"] = contentType
	}
	resp, err := c.publicShareRequest(ctx, http.MethodPut, token, filePath, content, password, headers)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		return &HTTPStatusError{Operation: "public share upload", StatusCode: resp.StatusCode}
	}
	return nil
}

func (c *Client) CreatePublicShareFolder(ctx context.Context, token, folderPath, password string) error {
	token = strings.TrimSpace(token)
	if token == "" {
		return ErrInvalidPublicShare
	}
	folderPath = NormalizeClientPath(folderPath)
	resp, err := c.publicShareRequest(ctx, "MKCOL", token, folderPath, nil, password, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusCreated {
		return &HTTPStatusError{Operation: "public share mkcol", StatusCode: resp.StatusCode}
	}
	return nil
}

func (c *Client) DeletePublicShare(ctx context.Context, token, filePath, password string) error {
	token = strings.TrimSpace(token)
	if token == "" {
		return ErrInvalidPublicShare
	}
	filePath = NormalizeClientPath(filePath)
	resp, err := c.publicShareRequest(ctx, http.MethodDelete, token, filePath, nil, password, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		return &HTTPStatusError{Operation: "public share delete", StatusCode: resp.StatusCode}
	}
	return nil
}

func (c *Client) MovePublicShare(ctx context.Context, token, srcPath, destPath, password string) error {
	token = strings.TrimSpace(token)
	if token == "" {
		return ErrInvalidPublicShare
	}
	srcPath = NormalizeClientPath(srcPath)
	destPath = NormalizeClientPath(destPath)
	srcDAV := publicShareDAVPath(token, srcPath)
	destDAV := publicShareDAVPath(token, destPath)
	resp, err := c.publicShareRequestRaw(ctx, "MOVE", srcDAV, nil, password, map[string]string{
		"Destination": c.webDAVDestination(destDAV),
		"Overwrite":   "F",
	})
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
		return ErrPublicSharePasswordRequired
	}
	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent {
		return &HTTPStatusError{Operation: "public share move", StatusCode: resp.StatusCode}
	}
	return nil
}
