package main

import (
	"context"
	"encoding/json"
	"encoding/xml"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/google/go-cmp/cmp"
)

var (
	postcode      = flag.String("postcode", "", "Postcode to look for")
	pushoverToken = flag.String("pushover_token", "", "Pushover application token")
	pushoverUser  = flag.String("pushover_user", "", "Pushover user token")

	checkIntervalError = flag.Duration("interval_error", 5*time.Minute, "Interval between loop reruns in the event of an error")
	checkInterval      = flag.Duration("interval", 30*time.Minute, "Interval between checks")
)

type Position struct {
	Lat float64 `xml:"lat,attr"`
	Lon float64 `xml:"lng,attr"`
}

func (p1 Position) Equal(p2 Position) bool {
	return p1.Lat == p2.Lat && p1.Lon == p2.Lon
}

type Locations struct {
	XMLName xml.Name `xml:"Locations"`

	Postcode string `xml:"postcode,attr"`
	Position

	StreetWorks []StreetWork `xml:"StreetWorks"`
}

// sortIfUnsorted returns the input slice if it's sorted, otherwise it creates a *new* slice that _is_ sorted.
func sortIfUnsorted(sw []StreetWork) []StreetWork {
	less := func(i, j int) bool {
		a, b := sw[i], sw[j]
		switch {
		case a.StartDate.Before(b.StartDate):
			return true
		case a.StartDate.After(b.StartDate):
			return false
		case a.Street < b.Street:
			return true
		case a.Street > b.Street:
			return false
		case a.Location < b.Location:
			return true
		default:
			return false
		}
	}

	if sort.SliceIsSorted(sw, less) {
		return sw
	}

	// Make a brand new slice...
	sw = append([]StreetWork(nil), sw...)
	sort.Slice(sw, less)
	return sw
}

func (l1 Locations) Equal(l2 Locations) bool {
	if l1.Postcode != l2.Postcode {
		return false
	}

	// Ignore Position, we don't care.

	if len(l1.StreetWorks) != len(l2.StreetWorks) {
		return false
	}

	l1s := sortIfUnsorted(l1.StreetWorks)
	l2s := sortIfUnsorted(l2.StreetWorks)

	for n := range l1s {
		a, b := l1s[n], l2s[n]
		if !a.Equal(b) {
			return false
		}
	}
	return true
}

type CamdenTime struct {
	time.Time
}

var CamdenTZ = func() *time.Location {
	l, err := time.LoadLocation("Europe/London")
	if err != nil {
		panic(fmt.Errorf("loading Europe/London timezone: %v", err))
	}
	return l
}()

func (t *CamdenTime) UnmarshalText(data []byte) error {
	rt, err := time.ParseInLocation("2006-01-02T15:04:05", string(data), CamdenTZ)
	if err == nil {
		t.Time = rt
	}
	return err
}

func (t CamdenTime) Equal(t2 CamdenTime) bool  { return t.Time.Equal(t2.Time) }
func (t CamdenTime) Before(t2 CamdenTime) bool { return t.Time.Before(t2.Time) }
func (t CamdenTime) After(t2 CamdenTime) bool  { return t.Time.After(t2.Time) }

type StreetWork struct {
	Location          string     `xml:"Location,attr"`
	StartDate         CamdenTime `xml:"StartDate,attr"` // RFC3339
	EndDate           CamdenTime `xml:"EndDate,attr"`   // RFC3339
	LocalAuthorityRef string     `xml:"LAref,attr"`
	Organisation      string     `xml:"Organisation,attr"`
	ExternalRef       string     `xml:"externalref,attr"`
	Telephone         string     `xml:"Telephone,attr"`
	Position
	Description         string `xml:"Description,attr"`
	Street              string `xml:"Street,attr"`
	Type                string `xml:"Type,attr"`
	OutOfHoursTelephone string `xml:"OutofHours,attr"`
}

func (sw1 StreetWork) Equal(sw2 StreetWork) bool {
	for _, b := range []bool{
		sw1.Location == sw2.Location,
		sw1.StartDate.Equal(sw2.StartDate),
		sw1.EndDate.Equal(sw2.EndDate),
		sw1.LocalAuthorityRef == sw2.LocalAuthorityRef,
		sw1.Organisation == sw2.Organisation,
		sw1.ExternalRef == sw2.ExternalRef,
		sw1.Telephone == sw2.Telephone,
		sw1.Position.Equal(sw2.Position),
		sw1.Description == sw2.Description,
		sw1.Street == sw2.Street,
		sw1.Type == sw2.Type,
		sw1.OutOfHoursTelephone == sw2.OutOfHoursTelephone,
	} {
		if !b {
			return false
		}
	}
	return true
}

func getStreetWorks(ctx context.Context, postcode string) (*Locations, error) {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, "GET", "https://find.camden.gov.uk/StreetWorksrest.aspx", nil)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequest: %w", err)
	}
	v := url.Values{}
	v.Set("area", postcode)
	req.URL.RawQuery = v.Encode()

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("making HTTP request: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("bad HTTP response code %v (%v)", resp.StatusCode, resp.Status)
	}

	l := new(Locations)
	if err := xml.NewDecoder(resp.Body).Decode(l); err != nil {
		return nil, fmt.Errorf("decoding XML response: %w", err)
	}
	if bs, err := io.ReadAll(resp.Body); err != nil {
		return nil, fmt.Errorf("checking for trailing data: %w", err)
	} else if len(bs) > 0 {
		return nil, fmt.Errorf("found trailing data (%d bytes; %q)", len(bs), string(bs))
	}

	return l, nil
}

type Notification struct {
	Message  string
	Title    string
	Priority int
	Sound    string
}

func (n Notification) EncodeValues() url.Values {
	v := url.Values{}
	v.Set("message", n.Message)
	if n.Title != "" {
		v.Set("title", n.Title)
	}
	if n.Priority != 0 {
		v.Set("priority", strconv.Itoa(n.Priority))
	}
	if n.Sound != "" {
		v.Set("sound", n.Sound)
	}
	return v
}

func sendPushoverNotification(ctx context.Context, token, user string, notification Notification) error {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
	defer cancel()

	v := notification.EncodeValues()
	v.Set("token", token)
	v.Set("user", user)

	req, err := http.NewRequestWithContext(ctx, "POST", "https://api.pushover.net/1/messages.json", strings.NewReader(v.Encode()))
	if err != nil {
		return fmt.Errorf("creating new HTTP request: %w", err)
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return fmt.Errorf("doing HTTP request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("bad HTTP response: %v (%q)", resp.StatusCode, resp.Status)
	}
	return nil
}

func main() {
	flag.Parse()

	if *postcode == "" {
		log.Fatalf("-postcode must be provided.")
	}

	ctx := context.Background()

	var lastWorks *Locations

	for {
		l, err := getStreetWorks(ctx, *postcode)
		if err != nil {
			log.Printf("getStreetWorks failed: %v; waiting 5 minutes", err)
			time.Sleep(*checkIntervalError)
			continue
		}
		log.Println("refreshed data...")

		if lastWorks != nil && !lastWorks.Equal(*l) {
			// OMG!!!
			lj, err := json.MarshalIndent(l, "", "  ")
			if err != nil {
				log.Fatalf("json.Marshal streetworks: %v", err)
			}

			log.Println("it's changed!")
			log.Println(string(lj))

			diff := cmp.Diff(lastWorks.StreetWorks, l.StreetWorks)
			if diff != "" {
				log.Println(diff)
				if *pushoverToken != "" && *pushoverUser != "" {
					if err := sendPushoverNotification(ctx, *pushoverToken, *pushoverUser, Notification{
						Title:    fmt.Sprintf("Camden StreetWorks Diff for %v", *postcode),
						Message:  diff,
						Priority: -1,
					}); err != nil {
						log.Printf("sendPushoverNotification: %v; waiting 5 minutes", err)
						time.Sleep(*checkIntervalError)
						continue
						// We'll see the diff again next time.
					}
				}
			}
		}
		lastWorks = l

		time.Sleep(*checkInterval)
	}
}