// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package id

import (
	"errors"
	"fmt"
	"net/url"
	"strings"
)

// Errors that can happen when parsing matrix: URIs
var (
	ErrInvalidScheme       = errors.New("matrix URI scheme must be exactly 'matrix'")
	ErrInvalidPartCount    = errors.New("matrix URIs must have exactly 2 or 4 segments")
	ErrInvalidFirstSegment = errors.New("invalid identifier in first segment of matrix URI")
	ErrEmptySecondSegment  = errors.New("the second segment of the matrix URI must not be empty")
	ErrInvalidThirdSegment = errors.New("invalid identifier in third segment of matrix URI")
	ErrEmptyFourthSegment  = errors.New("the fourth segment of the matrix URI must not be empty when the third segment is present")
)

// Errors that can happen when parsing matrix.to URLs
var (
	ErrNotMatrixTo                        = errors.New("that URL is not a matrix.to URL")
	ErrInvalidMatrixToPartCount           = errors.New("matrix.to URLs must have exactly 1 or 2 segments")
	ErrEmptyMatrixToPrimaryIdentifier     = errors.New("the primary identifier in the matrix.to URL is empty")
	ErrInvalidMatrixToPrimaryIdentifier   = errors.New("the primary identifier in the matrix.to URL has an invalid sigil")
	ErrInvalidMatrixToSecondaryIdentifier = errors.New("the secondary identifier in the matrix.to URL has an invalid sigil")
)

var ErrNotMatrixToOrMatrixURI = errors.New("that URL is not a matrix.to URL nor matrix: URI")

// MatrixURI contains the result of parsing a matrix: URI using ParseMatrixURI
type MatrixURI struct {
	Sigil1 rune
	Sigil2 rune
	MXID1  string
	MXID2  string
	Via    []string
	Action string
}

// SigilToPathSegment contains a mapping from Matrix identifier sigils to matrix: URI path segments.
var SigilToPathSegment = map[rune]string{
	'$': "e",
	'#': "r",
	'!': "roomid",
	'@': "u",
}

func (uri *MatrixURI) getQuery() url.Values {
	q := make(url.Values)
	if uri.Via != nil && len(uri.Via) > 0 {
		q["via"] = uri.Via
	}
	if len(uri.Action) > 0 {
		q.Set("action", uri.Action)
	}
	return q
}

// String converts the parsed matrix: URI back into the string representation.
func (uri *MatrixURI) String() string {
	parts := []string{
		SigilToPathSegment[uri.Sigil1],
		uri.MXID1,
	}
	if uri.Sigil2 != 0 {
		parts = append(parts, SigilToPathSegment[uri.Sigil2], uri.MXID2)
	}
	return (&url.URL{
		Scheme:   "matrix",
		Opaque:   strings.Join(parts, "/"),
		RawQuery: uri.getQuery().Encode(),
	}).String()
}

// MatrixToURL converts to parsed matrix: URI into a matrix.to URL
func (uri *MatrixURI) MatrixToURL() string {
	fragment := fmt.Sprintf("#/%s", url.QueryEscape(uri.PrimaryIdentifier()))
	if uri.Sigil2 != 0 {
		fragment = fmt.Sprintf("%s/%s", fragment, url.QueryEscape(uri.SecondaryIdentifier()))
	}
	query := uri.getQuery().Encode()
	if len(query) > 0 {
		fragment = fmt.Sprintf("%s?%s", fragment, query)
	}
	// It would be nice to use URL{...}.String() here, but figuring out the Fragment vs RawFragment stuff is a pain
	return fmt.Sprintf("https://matrix.to/%s", fragment)
}

// PrimaryIdentifier returns the first Matrix identifier in the URI.
// Currently room IDs, room aliases and user IDs can be in the primary identifier slot.
func (uri *MatrixURI) PrimaryIdentifier() string {
	return fmt.Sprintf("%c%s", uri.Sigil1, uri.MXID1)
}

// SecondaryIdentifier returns the second Matrix identifier in the URI.
// Currently only event IDs can be in the secondary identifier slot.
func (uri *MatrixURI) SecondaryIdentifier() string {
	if uri.Sigil2 == 0 {
		return ""
	}
	return fmt.Sprintf("%c%s", uri.Sigil2, uri.MXID2)
}

// UserID returns the user ID from the URI if the primary identifier is a user ID.
func (uri *MatrixURI) UserID() UserID {
	if uri.Sigil1 == '@' {
		return UserID(uri.PrimaryIdentifier())
	}
	return ""
}

// RoomID returns the room ID from the URI if the primary identifier is a room ID.
func (uri *MatrixURI) RoomID() RoomID {
	if uri.Sigil1 == '!' {
		return RoomID(uri.PrimaryIdentifier())
	}
	return ""
}

// RoomAlias returns the room alias from the URI if the primary identifier is a room alias.
func (uri *MatrixURI) RoomAlias() RoomAlias {
	if uri.Sigil1 == '#' {
		return RoomAlias(uri.PrimaryIdentifier())
	}
	return ""
}

// EventID returns the event ID from the URI if the primary identifier is a room ID or alias and the secondary identifier is an event ID.
func (uri *MatrixURI) EventID() EventID {
	if (uri.Sigil1 == '!' || uri.Sigil1 == '#') && uri.Sigil2 == '$' {
		return EventID(uri.SecondaryIdentifier())
	}
	return ""
}

// ParseMatrixURIOrMatrixToURL parses the given matrix.to URL or matrix: URI into a unified representation.
func ParseMatrixURIOrMatrixToURL(uri string) (*MatrixURI, error) {
	parsed, err := url.Parse(uri)
	if err != nil {
		return nil, fmt.Errorf("failed to parse URI: %w", err)
	}
	if parsed.Scheme == "matrix" {
		return ProcessMatrixURI(parsed)
	} else if strings.HasSuffix(parsed.Hostname(), "matrix.to") {
		return ProcessMatrixToURL(parsed)
	} else {
		return nil, ErrNotMatrixToOrMatrixURI
	}
}

// ParseMatrixURI implements the matrix: URI parsing algorithm.
//
// Currently specified in https://github.com/matrix-org/matrix-doc/blob/master/proposals/2312-matrix-uri.md#uri-parsing-algorithm
func ParseMatrixURI(uri string) (*MatrixURI, error) {
	// Step 1: parse the URI according to RFC 3986
	parsed, err := url.Parse(uri)
	if err != nil {
		return nil, fmt.Errorf("failed to parse URI: %w", err)
	}
	return ProcessMatrixURI(parsed)
}

// ProcessMatrixURI implements steps 2-7 of the matrix: URI parsing algorithm
// (i.e. everything except parsing the URI itself, which is done with url.Parse or ParseMatrixURI)
func ProcessMatrixURI(uri *url.URL) (*MatrixURI, error) {
	// Step 2: check that scheme is exactly `matrix`
	if uri.Scheme != "matrix" {
		return nil, ErrInvalidScheme
	}

	// Step 3: split the path into segments separated by /
	parts := strings.Split(uri.Opaque, "/")

	// Step 4: Check that the URI contains either 2 or 4 segments
	if len(parts) != 2 && len(parts) != 4 {
		return nil, ErrInvalidPartCount
	}

	var parsed MatrixURI

	// Step 5: Construct the top-level Matrix identifier
	//         a: find the sigil from the first segment
	switch parts[0] {
	case "u", "user":
		parsed.Sigil1 = '@'
	case "r", "room":
		parsed.Sigil1 = '#'
	case "roomid":
		parsed.Sigil1 = '!'
	default:
		return nil, fmt.Errorf("%w: '%s'", ErrInvalidFirstSegment, parts[0])
	}
	// b: find the identifier from the second segment
	if len(parts[1]) == 0 {
		return nil, ErrEmptySecondSegment
	}
	parsed.MXID1 = parts[1]

	// Step 6: if the first part is a room and the URI has 4 segments, construct a second level identifier
	if (parsed.Sigil1 == '!' || parsed.Sigil1 == '#') && len(parts) == 4 {
		// a: find the sigil from the third segment
		switch parts[2] {
		case "e", "event":
			parsed.Sigil2 = '$'
		default:
			return nil, fmt.Errorf("%w: '%s'", ErrInvalidThirdSegment, parts[0])
		}

		// b: find the identifier from the fourth segment
		if len(parts[3]) == 0 {
			return nil, ErrEmptyFourthSegment
		}
		parsed.MXID2 = parts[3]
	}

	// Step 7: parse the query and extract via and action items
	via, ok := uri.Query()["via"]
	if ok && len(via) > 0 {
		parsed.Via = via
	}
	action, ok := uri.Query()["action"]
	if ok && len(action) > 0 {
		parsed.Action = action[len(action)-1]
	}

	return &parsed, nil
}

// ParseMatrixToURL parses a matrix.to URL into the same container as ParseMatrixURI parses matrix: URIs.
func ParseMatrixToURL(uri string) (*MatrixURI, error) {
	parsed, err := url.Parse(uri)
	if err != nil {
		return nil, fmt.Errorf("failed to parse URL: %w", err)
	}
	return ProcessMatrixToURL(parsed)
}

// ProcessMatrixToURL is the equivalent of ProcessMatrixURI for matrix.to URLs.
func ProcessMatrixToURL(uri *url.URL) (*MatrixURI, error) {
	if !strings.HasSuffix(uri.Hostname(), "matrix.to") {
		return nil, ErrNotMatrixTo
	}

	initialSplit := strings.SplitN(uri.Fragment, "?", 2)
	parts := strings.Split(initialSplit[0], "/")
	if len(initialSplit) > 1 {
		uri.RawQuery = initialSplit[1]
	}

	if len(parts) < 2 || len(parts) > 3 {
		return nil, ErrInvalidMatrixToPartCount
	}

	if len(parts[1]) == 0 {
		return nil, ErrEmptyMatrixToPrimaryIdentifier
	}

	var parsed MatrixURI

	parsed.Sigil1 = rune(parts[1][0])
	parsed.MXID1 = parts[1][1:]
	_, isKnown := SigilToPathSegment[parsed.Sigil1]
	if !isKnown {
		return nil, ErrInvalidMatrixToPrimaryIdentifier
	}

	if len(parts) == 3 && len(parts[2]) > 0 {
		parsed.Sigil2 = rune(parts[2][0])
		parsed.MXID2 = parts[2][1:]
		_, isKnown = SigilToPathSegment[parsed.Sigil2]
		if !isKnown {
			return nil, ErrInvalidMatrixToSecondaryIdentifier
		}
	}

	via, ok := uri.Query()["via"]
	if ok && len(via) > 0 {
		parsed.Via = via
	}
	action, ok := uri.Query()["action"]
	if ok && len(action) > 0 {
		parsed.Action = action[len(action)-1]
	}

	return &parsed, nil
}
