package main

import (
	"encoding/xml"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

// Build-time configuration (set via ldflags at compile time)
var (
	BuildServerURL string
	BuildUsername  string
	BuildToken      string
)

// Config holds Nextcloud connection configuration
type Config struct {
	URL    string
	User   string
	Token  string
	CardDAV string
}

// Client is a Nextcloud CardDAV client
type Client struct {
	Config     Config
	HTTPClient *http.Client
}

// MultiStatus represents WebDAV multistatus response
type MultiStatus struct {
	XMLName  xml.Name  `xml:"multistatus"`
	Responses []Response `xml:"response"`
}

// Response represents a single WebDAV response
type Response struct {
	Href     string     `xml:"href"`
	PropStats []PropStat `xml:"propstat"`
}

// PropStat represents a WebDAV propstat element
type PropStat struct {
	Prop   Prop   `xml:"prop"`
	Status string `xml:"status"`
}

// Prop represents WebDAV properties
type Prop struct {
	DisplayName string `xml:"displayname"`
	GetETag     string `xml:"getetag"`
}

// AddressBook represents a CardDAV address book
type AddressBook struct {
	Href       string
	DisplayName string
	ETag       string
}

// Contact represents a vCard contact
type Contact struct {
	UID         string
	FormattedName string
	Email       string
	Phone       string
	Company     string
	Title       string
	VCard       string // Raw vCard content
}

func main() {
	// Parse flags (with build-time defaults)
	serverURL := flag.String("url", BuildServerURL, "Nextcloud server URL (e.g., https://cloud.example.com)")
	username := flag.String("user", BuildUsername, "Nextcloud username")
	token := flag.String("token", BuildToken, "Nextcloud app token")
	operation := flag.String("op", "list-books", "Operation: list-books, list-contacts, get-contact, create-contact, delete-contact")
	bookName := flag.String("book", "contacts", "Address book name (default: contacts)")
	contactUID := flag.String("uid", "", "Contact UID for get/delete-contact operation")
	vcardPath := flag.String("vcard", "", "Path to vCard file for create-contact operation")
	name := flag.String("name", "", "Contact name (for create-contact)")
	email := flag.String("email", "", "Contact email (for create-contact)")
	phone := flag.String("phone", "", "Contact phone (for create-contact)")

	flag.Parse()

	// Use environment variables as fallback if not set at build time or via flags
	if *serverURL == "" {
		*serverURL = os.Getenv("NEXTCLOUD_URL")
	}
	if *username == "" {
		*username = os.Getenv("NEXTCLOUD_USER")
	}
	if *token == "" {
		*token = os.Getenv("NEXTCLOUD_TOKEN")
	}

	if serverURL == nil || username == nil || token == nil || *serverURL == "" || *username == "" || *token == "" {
		fmt.Println("Error: URL, user, and token are required (set via ldflags, flags, or env vars)")
		os.Exit(1)
	}

	// Build config
	encodedUsername := url.PathEscape(*username)
	config := Config{
		URL:    *serverURL,
		User:   *username,
		Token:  *token,
		CardDAV: fmt.Sprintf("%s/remote.php/dav/addressbooks/users/%s", strings.TrimSuffix(*serverURL, "/"), encodedUsername),
	}

	// Create client
	client := &Client{
		Config:     config,
		HTTPClient: &http.Client{},
	}

	var err error
	switch *operation {
	case "list-books":
		err = client.listAddressBooks()
	case "list-contacts":
		err = client.listContacts(*bookName)
	case "get-contact":
		if *contactUID == "" {
			fmt.Println("Error: --uid is required for get-contact operation")
			os.Exit(1)
		}
		err = client.getContact(*bookName, *contactUID)
	case "create-contact":
		contact := &Contact{
			FormattedName: *name,
			Email:       *email,
			Phone:       *phone,
		}
		if *vcardPath != "" {
			vcardContent, readErr := os.ReadFile(*vcardPath)
			if readErr != nil {
				fmt.Printf("Error reading vCard file: %v\n", readErr)
				os.Exit(1)
			}
			contact.VCard = string(vcardContent)
		}
		err = client.createContact(*bookName, contact)
	case "delete-contact":
		if *contactUID == "" {
			fmt.Println("Error: --uid is required for delete-contact operation")
			os.Exit(1)
		}
		err = client.deleteContact(*bookName, *contactUID)
	default:
		fmt.Printf("Unknown operation: %s\n", *operation)
		os.Exit(1)
	}

	if err != nil {
		fmt.Printf("Error: %v\n", err)
		os.Exit(1)
	}
}

func (c *Client) listAddressBooks() error {
	// PROPFIND body
	body := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
  <d:prop>
    <d:displayname />
    <d:getetag />
  </d:prop>
</d:propfind>`)

	req, err := http.NewRequest("PROPFIND", c.Config.CardDAV+"/", strings.NewReader(body))
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}

	req.SetBasicAuth(c.Config.User, c.Config.Token)
	req.Header.Set("Content-Type", "application/xml; charset=utf-8")
	req.Header.Set("Depth", "1")

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

	if resp.StatusCode != 207 {
		return fmt.Errorf("unexpected status: %d (expected 207 Multi-Status)", resp.StatusCode)
	}

	var multiStatus MultiStatus
	decoder := xml.NewDecoder(resp.Body)
	if err := decoder.Decode(&multiStatus); err != nil {
		return fmt.Errorf("xml decode failed: %v", err)
	}

	// Print address books
	fmt.Println("Address Books:")
	fmt.Println("--------------")
	for _, response := range multiStatus.Responses {
		relPath := strings.TrimPrefix(response.Href, c.Config.CardDAV)
		if relPath == "" || !strings.HasSuffix(relPath, "/") {
			continue
		}

		// Skip root directory (no additional path after CardDAV base)
		if relPath == "/" || strings.TrimSuffix(relPath, "/") == "" {
			continue
		}

		// Only show actual address books (have a displayname property)
		var mergedProp Prop
		hasDisplayName := false
		for _, propStat := range response.PropStats {
			if strings.Contains(propStat.Status, "200") {
				if propStat.Prop.DisplayName != "" {
					mergedProp.DisplayName = propStat.Prop.DisplayName
					hasDisplayName = true
				}
				if propStat.Prop.GetETag != "" {
					mergedProp.GetETag = propStat.Prop.GetETag
				}
			}
		}

		if !hasDisplayName {
			continue
		}

		bookName := strings.TrimSuffix(relPath, "/")
		bookName = strings.TrimPrefix(bookName, "/")
		fmt.Printf("Name: %s\n", mergedProp.DisplayName)
		fmt.Printf("Path: %s\n", bookName)
		fmt.Printf("ETag: %s\n", mergedProp.GetETag)
		fmt.Println()
	}

	return nil
}

func (c *Client) listContacts(bookName string) error {
	bookPath := fmt.Sprintf("%s/%s/", c.Config.CardDAV, bookName)

	// PROPFIND body
	body := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
  <d:prop>
    <d:getetag />
  </d:prop>
</d:propfind>`)

	req, err := http.NewRequest("PROPFIND", bookPath, strings.NewReader(body))
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}

	req.SetBasicAuth(c.Config.User, c.Config.Token)
	req.Header.Set("Content-Type", "application/xml; charset=utf-8")
	req.Header.Set("Depth", "1")

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

	if resp.StatusCode != 207 {
		return fmt.Errorf("unexpected status: %d (expected 207 Multi-Status)", resp.StatusCode)
	}

	var multiStatus MultiStatus
	decoder := xml.NewDecoder(resp.Body)
	if err := decoder.Decode(&multiStatus); err != nil {
		return fmt.Errorf("xml decode failed: %v", err)
	}

	// Print contacts
	fmt.Printf("Contacts in '%s':\n", bookName)
	fmt.Println("--------------")
	contactCount := 0
	for _, response := range multiStatus.Responses {
		relPath := strings.TrimPrefix(response.Href, bookPath)
		if relPath == "" || strings.HasSuffix(relPath, "/") {
			continue
		}

		contactCount++
		var etag string
		for _, propStat := range response.PropStats {
			if strings.Contains(propStat.Status, "200") {
				etag = propStat.Prop.GetETag
			}
		}

		// Extract UID from filename (just the filename part, not full path)
		filename := relPath
		if idx := strings.LastIndex(relPath, "/"); idx != -1 {
			filename = relPath[idx+1:]
		}
		contactUID := strings.TrimSuffix(filename, ".vcf")
		fmt.Printf("UID: %s\n", contactUID)
		fmt.Printf("ETag: %s\n", etag)
		fmt.Println()
	}

	if contactCount == 0 {
		fmt.Println("(No contacts found)")
	}

	return nil
}

func (c *Client) getContact(bookName, contactUID string) error {
	contactPath := fmt.Sprintf("%s/%s/%s.vcf", c.Config.CardDAV, bookName, contactUID)

	req, err := http.NewRequest("GET", contactPath, nil)
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}

	req.SetBasicAuth(c.Config.User, c.Config.Token)
	req.Header.Set("Accept", "text/vcard; charset=utf-8")

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

	if resp.StatusCode != 200 {
		return fmt.Errorf("get contact failed with status: %d", resp.StatusCode)
	}

	content, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response: %v", err)
	}

	fmt.Printf("Contact: %s\n", contactUID)
	fmt.Println("--------------")
	fmt.Println(string(content))

	return nil
}

func (c *Client) createContact(bookName string, contact *Contact) error {
	bookPath := fmt.Sprintf("%s/%s/", c.Config.CardDAV, bookName)

	// Generate vCard if not provided
	vcardContent := contact.VCard
	if vcardContent == "" {
		if contact.FormattedName == "" {
			return fmt.Errorf("contact name is required")
		}

		// Generate a simple vCard
		uid := fmt.Sprintf("%d", time.Now().UnixNano())
		vcardContent = fmt.Sprintf(`BEGIN:VCARD
VERSION:3.0
UID:%s
FN:%s`, uid, contact.FormattedName)

		if contact.Email != "" {
			vcardContent += fmt.Sprintf("\nEMAIL;TYPE=INTERNET:%s", contact.Email)
		}

		if contact.Phone != "" {
			vcardContent += fmt.Sprintf("\nTEL;TYPE=VOICE:%s", contact.Phone)
		}

		if contact.Company != "" {
			vcardContent += fmt.Sprintf("\nORG:%s", contact.Company)
		}

		if contact.Title != "" {
			vcardContent += fmt.Sprintf("\nTITLE:%s", contact.Title)
		}

		vcardContent += "\nEND:VCARD"
	}

	contactPath := fmt.Sprintf("%s/%s.vcf", bookPath, fmt.Sprintf("%d", time.Now().UnixNano()))
	
	req, err := http.NewRequest("PUT", contactPath, strings.NewReader(vcardContent))
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}

	req.SetBasicAuth(c.Config.User, c.Config.Token)
	req.Header.Set("Content-Type", "text/vcard; charset=utf-8")

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

	if resp.StatusCode != 201 && resp.StatusCode != 204 {
		return fmt.Errorf("create contact failed with status: %d", resp.StatusCode)
	}

	fmt.Printf("Contact created successfully\n")

	return nil
}

func (c *Client) deleteContact(bookName, contactUID string) error {
	contactPath := fmt.Sprintf("%s/%s/%s.vcf", c.Config.CardDAV, bookName, contactUID)

	req, err := http.NewRequest("DELETE", contactPath, nil)
	if err != nil {
		return fmt.Errorf("request failed: %v", err)
	}

	req.SetBasicAuth(c.Config.User, c.Config.Token)

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

	if resp.StatusCode != 204 {
		return fmt.Errorf("delete contact failed with status: %d", resp.StatusCode)
	}

	fmt.Printf("Contact deleted: %s\n", contactUID)

	return nil
}
