package nextcloud

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

type Calendar struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name"`
	Color       string `json:"color"`
	Path        string `json:"path"`
}

type Event struct {
	UID         string          `json:"uid"`
	Summary     string          `json:"summary"`
	Description string          `json:"description"`
	Location    string          `json:"location"`
	Start       string          `json:"start"`
	End         string          `json:"end"`
	AllDay      bool            `json:"all_day"`
	Path        string          `json:"path,omitempty"`
	ETag        string          `json:"etag,omitempty"`
	Organizer   string          `json:"organizer,omitempty"`
	Attendees   []EventAttendee `json:"attendees,omitempty"`
	MeetURL     string          `json:"meet_url,omitempty"`
	RawICS      string          `json:"raw_ics,omitempty"`
}

type EventAttendee struct {
	Email  string `json:"email"`
	Name   string `json:"name,omitempty"`
	Status string `json:"status,omitempty"`
	Role   string `json:"role,omitempty"`
}

var (
	ErrETagMismatch     = errors.New("nextcloud calendar etag mismatch")
	ErrAttendeeNotFound = errors.New("nextcloud calendar attendee not found")
)

type FreeBusyRequest struct {
	Start     time.Time
	End       time.Time
	Attendees []string
}

type FreeBusyPeriod struct {
	Start string `json:"start"`
	End   string `json:"end"`
	Type  string `json:"type,omitempty"`
}

type AttendeeFreeBusy struct {
	Email   string           `json:"email"`
	Periods []FreeBusyPeriod `json:"periods"`
}

type FreeBusyResponse struct {
	Attendees []AttendeeFreeBusy `json:"attendees"`
}

func (c *Client) ListCalendars(ctx context.Context, userID string) ([]Calendar, error) {
	path := fmt.Sprintf("/remote.php/dav/calendars/%s/", userID)
	body := `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:cs="urn:ietf:params:xml:ns:caldav" xmlns:apple="http://apple.com/ns/ical/">
  <d:prop>
    <d:displayname/>
    <apple:calendar-color/>
    <d:resourcetype/>
  </d:prop>
</d:propfind>`

	resp, err := c.DoAsUser(ctx, "PROPFIND", path, strings.NewReader(body), userID, map[string]string{
		"Depth":        "1",
		"Content-Type": "application/xml",
	})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return parseCalendarList(resp.Body, path)
}

func (c *Client) ListEvents(ctx context.Context, userID, calendarPath string, from, to time.Time) ([]Event, error) {
	body := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
  <d:prop>
    <d:getetag/>
    <c:calendar-data/>
  </d:prop>
  <c:filter>
    <c:comp-filter name="VCALENDAR">
      <c:comp-filter name="VEVENT">
        <c:time-range start="%s" end="%s"/>
      </c:comp-filter>
    </c:comp-filter>
  </c:filter>
</c:calendar-query>`, from.Format("20060102T150405Z"), to.Format("20060102T150405Z"))

	resp, err := c.DoAsUser(ctx, "REPORT", calendarPath, strings.NewReader(body), userID, map[string]string{
		"Depth":        "1",
		"Content-Type": "application/xml",
	})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return parseEventList(resp.Body)
}

func (c *Client) CreateEvent(ctx context.Context, userID, calendarPath string, event *Event) error {
	ics := buildICS(event)
	uid := event.UID
	if uid == "" {
		uid = fmt.Sprintf("%d@ulti", time.Now().UnixNano())
	}
	eventPath := fmt.Sprintf("%s%s.ics", calendarPath, uid)

	resp, err := c.DoAsUser(ctx, "PUT", eventPath, strings.NewReader(ics), userID, map[string]string{
		"Content-Type": "text/calendar; charset=utf-8",
	})
	if err != nil {
		return err
	}
	defer resp.Body.Close()

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

func (c *Client) GetEvent(ctx context.Context, userID, eventPath string) (*Event, error) {
	resp, err := c.DoAsUser(ctx, "GET", eventPath, nil, userID, nil)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("get event failed: %d", resp.StatusCode)
	}
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	event := parseICS(string(raw))
	event.RawICS = string(raw)
	event.Path = eventPath
	event.ETag = strings.TrimSpace(resp.Header.Get("ETag"))
	return &event, nil
}

func (c *Client) UpdateEvent(ctx context.Context, userID, eventPath, ifMatch string, event *Event) (string, error) {
	ics := buildICS(event)
	headers := map[string]string{
		"Content-Type": "text/calendar; charset=utf-8",
	}
	if strings.TrimSpace(ifMatch) != "" {
		headers["If-Match"] = strings.TrimSpace(ifMatch)
	}

	resp, err := c.DoAsUser(ctx, "PUT", eventPath, strings.NewReader(ics), userID, headers)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusPreconditionFailed {
		return "", ErrETagMismatch
	}
	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusCreated {
		return "", fmt.Errorf("update event failed: %d", resp.StatusCode)
	}
	return strings.TrimSpace(resp.Header.Get("ETag")), nil
}

func (c *Client) DeleteEvent(ctx context.Context, userID, eventPath string) error {
	resp, err := c.DoAsUser(ctx, "DELETE", eventPath, nil, userID, nil)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 204 {
		return fmt.Errorf("delete event failed: %d", resp.StatusCode)
	}
	return nil
}

func (c *Client) FreeBusy(ctx context.Context, userID string, req *FreeBusyRequest) (*FreeBusyResponse, error) {
	path := fmt.Sprintf("/remote.php/dav/calendars/%s/outbox/", userID)
	body := buildFreeBusyRequestICS(userID, req)

	resp, err := c.DoAsUser(ctx, "POST", path, strings.NewReader(body), userID, map[string]string{
		"Content-Type": "text/calendar; charset=utf-8",
	})
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusMultiStatus {
		return nil, fmt.Errorf("free/busy request failed: %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
	}

	return parseFreeBusyResponse(raw)
}

func buildFreeBusyRequestICS(userID string, req *FreeBusyRequest) string {
	var b strings.Builder
	b.WriteString("BEGIN:VCALENDAR\r\n")
	b.WriteString("VERSION:2.0\r\n")
	b.WriteString("PRODID:-//Ulti Suite//EN\r\n")
	b.WriteString("METHOD:REQUEST\r\n")
	b.WriteString("BEGIN:VFREEBUSY\r\n")
	b.WriteString(fmt.Sprintf("UID:fb-%d@ulti\r\n", time.Now().UnixNano()))
	b.WriteString(fmt.Sprintf("ORGANIZER:mailto:%s\r\n", userID))
	b.WriteString(fmt.Sprintf("DTSTAMP:%s\r\n", time.Now().UTC().Format("20060102T150405Z")))
	b.WriteString(fmt.Sprintf("DTSTART:%s\r\n", req.Start.UTC().Format("20060102T150405Z")))
	b.WriteString(fmt.Sprintf("DTEND:%s\r\n", req.End.UTC().Format("20060102T150405Z")))
	for _, email := range req.Attendees {
		addr := strings.TrimSpace(email)
		if !strings.HasPrefix(strings.ToLower(addr), "mailto:") {
			addr = "mailto:" + addr
		}
		b.WriteString(fmt.Sprintf("ATTENDEE:%s\r\n", addr))
	}
	b.WriteString("END:VFREEBUSY\r\n")
	b.WriteString("END:VCALENDAR\r\n")
	return b.String()
}

func parseFreeBusyResponse(raw []byte) (*FreeBusyResponse, error) {
	text := strings.TrimSpace(string(raw))
	if text == "" {
		return &FreeBusyResponse{Attendees: []AttendeeFreeBusy{}}, nil
	}
	if strings.HasPrefix(text, "<?xml") || strings.HasPrefix(text, "<") {
		var ms calMultistatus
		if err := xml.Unmarshal(raw, &ms); err == nil && len(ms.Responses) > 0 {
			attendees := make([]AttendeeFreeBusy, 0, len(ms.Responses))
			for _, r := range ms.Responses {
				if r.Propstat.Prop.CalendarData != "" {
					parsed := parseFreeBusyICS(r.Propstat.Prop.CalendarData)
					attendees = append(attendees, parsed.Attendees...)
				}
			}
			if len(attendees) > 0 {
				return &FreeBusyResponse{Attendees: attendees}, nil
			}
		}
	}
	return parseFreeBusyICS(text), nil
}

func parseFreeBusyICS(ics string) *FreeBusyResponse {
	resp := &FreeBusyResponse{Attendees: []AttendeeFreeBusy{}}
	inFreeBusy := false
	curIdx := -1

	for _, line := range strings.Split(ics, "\n") {
		line = strings.TrimSpace(strings.TrimSuffix(line, "\r"))
		switch {
		case line == "BEGIN:VFREEBUSY":
			inFreeBusy = true
			curIdx = -1
		case line == "END:VFREEBUSY":
			inFreeBusy = false
			curIdx = -1
		case inFreeBusy && strings.HasPrefix(line, "ATTENDEE"):
			resp.Attendees = append(resp.Attendees, AttendeeFreeBusy{
				Email: extractMailto(extractValue(line)),
			})
			curIdx = len(resp.Attendees) - 1
		case inFreeBusy && strings.HasPrefix(line, "FREEBUSY") && curIdx >= 0:
			period := parseFreeBusyLine(line)
			resp.Attendees[curIdx].Periods = append(resp.Attendees[curIdx].Periods, period)
		}
	}
	return resp
}

func parseFreeBusyLine(line string) FreeBusyPeriod {
	period := FreeBusyPeriod{Type: "BUSY"}
	value := extractValue(line)
	if idx := strings.Index(value, "/"); idx >= 0 {
		period.Start = value[:idx]
		period.End = value[idx+1:]
	}
	if paramPart, _, ok := strings.Cut(line, ":"); ok {
		for _, param := range strings.Split(paramPart, ";")[1:] {
			if strings.HasPrefix(strings.ToUpper(param), "FBTYPE=") {
				period.Type = strings.TrimPrefix(strings.ToUpper(param), "FBTYPE=")
			}
		}
	}
	return period
}

func extractMailto(value string) string {
	value = strings.TrimSpace(value)
	if idx := strings.Index(strings.ToLower(value), "mailto:"); idx >= 0 {
		return value[idx+7:]
	}
	return value
}

func buildICS(event *Event) string {
	var b strings.Builder
	b.WriteString("BEGIN:VCALENDAR\r\n")
	b.WriteString("VERSION:2.0\r\n")
	b.WriteString("PRODID:-//Ulti Suite//EN\r\n")
	b.WriteString("BEGIN:VEVENT\r\n")
	if event.UID != "" {
		b.WriteString(fmt.Sprintf("UID:%s\r\n", event.UID))
	}
	b.WriteString(fmt.Sprintf("SUMMARY:%s\r\n", event.Summary))
	if event.Description != "" {
		b.WriteString(fmt.Sprintf("DESCRIPTION:%s\r\n", event.Description))
	}
	if event.Location != "" {
		b.WriteString(fmt.Sprintf("LOCATION:%s\r\n", event.Location))
	}
	if event.Organizer != "" {
		b.WriteString(fmt.Sprintf("ORGANIZER:mailto:%s\r\n", event.Organizer))
	}
	for _, attendee := range event.Attendees {
		email := strings.TrimSpace(attendee.Email)
		if email == "" {
			continue
		}
		status := normalizeAttendeeStatus(attendee.Status)
		role := strings.ToUpper(strings.TrimSpace(attendee.Role))
		if role == "" {
			role = "REQ-PARTICIPANT"
		}
		line := fmt.Sprintf("ATTENDEE;PARTSTAT=%s;ROLE=%s", status, role)
		if strings.TrimSpace(attendee.Name) != "" {
			line += fmt.Sprintf(";CN=\"%s\"", strings.TrimSpace(attendee.Name))
		}
		line += fmt.Sprintf(":mailto:%s\r\n", email)
		b.WriteString(line)
	}
	if strings.TrimSpace(event.MeetURL) != "" {
		b.WriteString(fmt.Sprintf("URL:%s\r\n", strings.TrimSpace(event.MeetURL)))
		b.WriteString(fmt.Sprintf("X-ULTI-MEET-URL:%s\r\n", strings.TrimSpace(event.MeetURL)))
	}
	b.WriteString(fmt.Sprintf("DTSTART:%s\r\n", event.Start))
	b.WriteString(fmt.Sprintf("DTEND:%s\r\n", event.End))
	b.WriteString(fmt.Sprintf("DTSTAMP:%s\r\n", time.Now().UTC().Format("20060102T150405Z")))
	b.WriteString("END:VEVENT\r\n")
	b.WriteString("END:VCALENDAR\r\n")
	return b.String()
}

func parseCalendarList(body io.Reader, basePath string) ([]Calendar, error) {
	var ms multistatus
	if err := xml.NewDecoder(body).Decode(&ms); err != nil {
		return nil, err
	}

	calendars := make([]Calendar, 0)
	for _, r := range ms.Responses {
		if r.Href == basePath {
			continue
		}
		name := r.Propstat.Prop.DisplayName
		if name == "" {
			continue
		}
		calendars = append(calendars, Calendar{
			ID:          strings.TrimSuffix(strings.TrimPrefix(r.Href, basePath), "/"),
			DisplayName: name,
			Color:       r.Propstat.Prop.CalendarColor,
			Path:        r.Href,
		})
	}
	return calendars, nil
}

func parseEventList(body io.Reader) ([]Event, error) {
	var ms calMultistatus
	if err := xml.NewDecoder(body).Decode(&ms); err != nil {
		return nil, err
	}

	events := make([]Event, 0, len(ms.Responses))
	for _, r := range ms.Responses {
		ics := r.Propstat.Prop.CalendarData
		event := parseICS(ics)
		event.RawICS = ics
		event.Path = r.Href
		event.ETag = strings.TrimSpace(r.Propstat.Prop.ETag)
		events = append(events, event)
	}
	return events, nil
}

func parseICS(ics string) Event {
	var e Event
	for _, line := range strings.Split(ics, "\n") {
		line = strings.TrimSpace(line)
		switch {
		case strings.HasPrefix(line, "UID:"):
			e.UID = strings.TrimPrefix(line, "UID:")
		case strings.HasPrefix(line, "SUMMARY:"):
			e.Summary = strings.TrimPrefix(line, "SUMMARY:")
		case strings.HasPrefix(line, "DESCRIPTION:"):
			e.Description = strings.TrimPrefix(line, "DESCRIPTION:")
		case strings.HasPrefix(line, "LOCATION:"):
			e.Location = strings.TrimPrefix(line, "LOCATION:")
		case strings.HasPrefix(line, "ORGANIZER"):
			e.Organizer = extractMailto(extractValue(line))
		case strings.HasPrefix(line, "ATTENDEE"):
			e.Attendees = append(e.Attendees, parseAttendeeLine(line))
		case strings.HasPrefix(line, "X-ULTI-MEET-URL:"):
			e.MeetURL = strings.TrimPrefix(line, "X-ULTI-MEET-URL:")
		case strings.HasPrefix(line, "URL:") && e.MeetURL == "":
			e.MeetURL = strings.TrimPrefix(line, "URL:")
		case strings.HasPrefix(line, "DTSTART"):
			e.Start = extractValue(line)
		case strings.HasPrefix(line, "DTEND"):
			e.End = extractValue(line)
		}
	}
	return e
}

func extractValue(line string) string {
	if idx := strings.LastIndex(line, ":"); idx >= 0 {
		return line[idx+1:]
	}
	return line
}

func parseAttendeeLine(line string) EventAttendee {
	attendee := EventAttendee{Status: "NEEDS-ACTION"}
	paramPart, value, ok := strings.Cut(line, ":")
	if !ok {
		return attendee
	}
	attendee.Email = extractMailto(value)
	parts := strings.Split(paramPart, ";")
	for _, p := range parts[1:] {
		key, val, found := strings.Cut(p, "=")
		if !found {
			continue
		}
		switch strings.ToUpper(strings.TrimSpace(key)) {
		case "PARTSTAT":
			attendee.Status = strings.ToUpper(strings.TrimSpace(val))
		case "CN":
			attendee.Name = strings.Trim(strings.TrimSpace(val), "\"")
		case "ROLE":
			attendee.Role = strings.ToUpper(strings.TrimSpace(val))
		}
	}
	return attendee
}

func normalizeAttendeeStatus(status string) string {
	status = strings.ToUpper(strings.TrimSpace(status))
	switch status {
	case "ACCEPTED", "DECLINED", "TENTATIVE", "NEEDS-ACTION":
		return status
	default:
		return "NEEDS-ACTION"
	}
}

type calMultistatus struct {
	XMLName   xml.Name      `xml:"multistatus"`
	Responses []calResponse `xml:"response"`
}

type calResponse struct {
	Href     string      `xml:"href"`
	Propstat calPropstat `xml:"propstat"`
}

type calPropstat struct {
	Prop calProp `xml:"prop"`
}

type calProp struct {
	ETag         string `xml:"getetag"`
	CalendarData string `xml:"calendar-data"`
}
