package web_driver

// |@@| C

import (
	"context"
	"errors"
	"gardening/src/lib"
	erro "gardening/src/lib/error"
	"gardening/src/lib/logger"
	"gardening/src/lib/web_server"
	"github.com/chromedp/cdproto/cdp"
	"github.com/chromedp/chromedp"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

type WebDriver struct {
	WebServer *web_server.WebServer
	Logger    *logger.Logger
	Data      struct {
		Initialized bool
		Cancel1     context.CancelFunc
		Cancel2     context.CancelFunc
	}
}

func (this *WebDriver) StartSession(ctx context.Context) (ctx2 context.Context) {
	this.WebServer.Setup(ctx)
	go this.WebServer.ListenAndServe(ctx)

	// Wait for server to be ready before setting up WebDriver
	url := "http://localhost:" + this.WebServer.GetPort()
	if err := this.WaitForServer(url, 10*time.Second); err != nil {
		panic(err)
	}

	ctx2, _ = this.SetWebDriveIntoContext(ctx)

	this.Data.Initialized = true
	return ctx2
}

func (this *WebDriver) StopSession(ctx context.Context) {
	this.WebServer.MustShutdown(ctx)

	timeoutCtx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	done := make(chan bool, 1)

	go func() {
		// Have to call this, despite normally it should be handled via the cancel
		chromedp.Cancel(ctx)

		this.Data.Cancel2()
		this.Data.Cancel1()

		done <- true
	}()

	select {
	case <-done:
		this.Logger.Info(ctx, "Chrome closed successfully")
	case <-timeoutCtx.Done():
		this.Logger.Info(ctx, "WebDriver stopped successfully")
	}
}

func (this *WebDriver) WaitForServer(url string, timeout time.Duration) error {
	client := &http.Client{Timeout: 1 * time.Second}
	deadline := time.Now().Add(timeout)

	for time.Now().Before(deadline) {
		resp, err := client.Get(url)
		if err == nil {
			resp.Body.Close()
			return nil
		}
		time.Sleep(50 * time.Millisecond)
	}
	return erro.N("Server did not become ready within timeout").KV("url", url)
}

func (this *WebDriver) SetWebDriveIntoContext(ctx context.Context) (context.Context, context.CancelFunc) {
	opts := append(chromedp.DefaultExecAllocatorOptions[:],
		chromedp.Flag("headless", false),
	)
	ctx, cancel1 := chromedp.NewExecAllocator(ctx, opts...)
	ctx, cancel2 := chromedp.NewContext(
		ctx,
		//chromedp.WithDebugf(log.Printf),
	)
	this.Data.Cancel1 = cancel1
	this.Data.Cancel2 = cancel2

	return ctx, cancel2
}

func (this *WebDriver) Run(ctx context.Context, actions ...chromedp.Action) error {
	if this.Data.Initialized == false {
		panic(erro.N("Not initialized yet"))
	}
	return chromedp.Run(ctx, actions...)
}

func (this *WebDriver) Navigate(ctx context.Context, url string) error {
	url = "http://localhost:" + this.WebServer.GetPort() + url
	return this.Run(ctx, chromedp.Navigate(url))
}

func (this *WebDriver) MustNavigate(ctx context.Context, url string) {
	err := this.Navigate(ctx, url)
	lib.Poe(err)
}

func (this *WebDriver) WaitVisible(ctx context.Context, sel string) {
	timeout := 5 * time.Second
	ctxWithTimeout, cancelTimeout := context.WithTimeout(ctx, timeout)
	defer cancelTimeout()
	err := this.Run(ctxWithTimeout, chromedp.WaitVisible(sel))
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			println("WaitVisible timeout triggered for selector:", sel)
		}
		panic(err)
	}
}

func (this *WebDriver) WaitReady(ctx context.Context, sel string) error {
	timeout := 5 * time.Second
	ctxWithTimeout, cancelTimeout := context.WithTimeout(ctx, timeout)
	defer cancelTimeout()
	err := this.Run(ctxWithTimeout, chromedp.WaitReady(sel))
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			println("WaitReady timeout triggered for selector:", sel)
		}
		return err
	}
	return nil
}

func (this *WebDriver) WaitForCurrentUrl(ctx context.Context, expectedUrl string) error {
	timeout := 5 * time.Second
	ctxWithTimeout, cancelTimeout := context.WithTimeout(ctx, timeout)
	defer cancelTimeout()

	expectedUrl = "http://localhost:" + this.WebServer.GetPort() + expectedUrl

	for {
		select {
		case <-ctxWithTimeout.Done():
			currentUrl := this.GetCurrentURL(ctx)
			println("WaitForCurrentUrl timeout triggered. Expected:", expectedUrl, "Got:", currentUrl)
			return context.DeadlineExceeded
		default:
			currentUrl := this.GetCurrentURL(ctx)
			if currentUrl == expectedUrl {
				return nil
			}
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func (this *WebDriver) MustWaitForCurrentUrl(ctx context.Context, expectedUrl string) {
	err := this.WaitForCurrentUrl(ctx, expectedUrl)
	lib.Poe(err)
}

func (this *WebDriver) MustWaitForText(ctx context.Context, text string) {
	err := this.WaitForText(ctx, text)
	lib.Poe(err)
}

func (this *WebDriver) WaitForText(ctx context.Context, text string) error {
	timeout := 5 * time.Second
	ctxWithTimeout, cancelTimeout := context.WithTimeout(ctx, timeout)
	defer cancelTimeout()

	for {
		select {
		case <-ctxWithTimeout.Done():
			pageHTML := this.GetPageHTML(ctx)
			if pageHTML != "" {
				// Save the HTML to file for debugging
				os.WriteFile("timeout_page.html", []byte(pageHTML), 0644)
				this.Logger.Info(ctx, "Page HTML saved to timeout_page.html for debugging")
			}
			return context.DeadlineExceeded
		default:
			pageHTML := this.GetPageHTML(ctx)
			if pageHTML != "" && strings.Contains(pageHTML, text) {
				return nil
			}
			time.Sleep(100 * time.Millisecond)
		}
	}
}

func (this *WebDriver) WaitVisibleUntil(ctx context.Context, sel string) error {
	timeout := 5 * time.Second
	ctxWithTimeout, cancelTimeout := context.WithTimeout(ctx, timeout)
	defer cancelTimeout()
	return this.Run(ctxWithTimeout, chromedp.WaitVisible(sel))
}

func (this *WebDriver) Click(ctx context.Context, sel string) error {
	return this.Run(ctx, chromedp.Click(sel))
}

func (this *WebDriver) SendKeys(ctx context.Context, sel string, v string) error {
	return this.Run(ctx, chromedp.SendKeys(sel, v))
}

func (this *WebDriver) TenSecondPause(ctx context.Context) {
	time.Sleep(10 * time.Second)
}

func (this *WebDriver) OneSecondPause(ctx context.Context) {
	time.Sleep(1 * time.Second)
}

func (this *WebDriver) TakeFullScreenshot(ctx context.Context) (err error) {
	var buf []byte
	err = this.Run(ctx, chromedp.FullScreenshot(&buf, 90))
	if err != nil {
		return err
	}
	err = os.WriteFile("fullScreenshot.png", buf, 0o644)
	if err != nil {
		return err
	}
	return nil
}

func (this *WebDriver) GetPageHTML(ctx context.Context) (html string) {
	err := chromedp.Run(ctx, chromedp.OuterHTML("html", &html))
	if err != nil {
		log.Printf("Error getting page HTML: %v", err)
		// Try alternative approach with JavaScript
		err = chromedp.Run(ctx, chromedp.Evaluate(
			`document.documentElement.outerHTML`,
			&html,
		))
		if err != nil {
			log.Printf("Error getting page HTML via JS: %v", err)
		}
	}
	return html
}

func (this *WebDriver) GetOuterHTML(ctx context.Context, selector string) string {
	var html string
	this.Run(ctx, chromedp.OuterHTML(selector, &html))
	return html
}

func (this *WebDriver) GetInnerHTML(ctx context.Context, selector string) string {
	var html string
	this.Run(ctx, chromedp.InnerHTML(selector, &html))
	return html
}

func (this *WebDriver) GetCurrentURL(ctx context.Context) string {
	var url string
	err := chromedp.Run(ctx, chromedp.Location(&url))
	if err != nil {
		log.Printf("Error getting current URL: %v", err)
		return ""
	}
	return url
}

func (this *WebDriver) ClickOnButtonViaLabel(ctx context.Context, label string) (err error) {
	// Use XPath to find elements with role="button" and exact text match
	sel := `//button[@role="button" and normalize-space(text())="` + label + `"] | //*[@role="button" and normalize-space(text())="` + label + `"]`

	nodes, err := this.bySearch(ctx, sel)
	if err != nil {
		return err
	}
	err = this.onlyOneNode(nodes, label, sel)
	if err != nil {
		return err
	}

	return this.Click(ctx, sel)
}

func (this *WebDriver) onlyOneNode(nodes []*cdp.Node, label string, sel string) error {
	if len(nodes) == 0 {
		return erro.N("No button found with the specified label").KV("label", label).KV("sel", sel)
	}
	if len(nodes) > 1 {
		return erro.N("Multiple buttons found with the same label").KV("label", label).KV("sel", sel).KV("count", len(nodes))
	}
	return nil
}

func (this *WebDriver) MustClickOnButtonViaLabel(ctx context.Context, label string) {
	err := this.ClickOnButtonViaLabel(ctx, label)
	lib.Poe(err)
}

func (this *WebDriver) Login(ctx context.Context) (err error) {
	url := "/auth/app-login?message=unauthorized"
	urlSuccess := "/app"

	err = this.Navigate(ctx, url)
	if err != nil {
		return err
	}
	err = this.WaitReady(ctx, "body")
	if err != nil {
		return err
	}
	err = this.Click(ctx, `/html/body/div/form/button`)
	if err != nil {
		return err
	}

	err = this.WaitForCurrentUrl(ctx, urlSuccess)
	if err != nil {
		panic(erro.W("Login failed. Expected redirect to "+urlSuccess+" but timeout occurred", err))
	}
	this.Logger.Info(ctx, "Login success")
	return nil
}

func (this *WebDriver) bySearch(ctx context.Context, sel string) ([]*cdp.Node, error) {
	timeout := 3 * time.Second
	ctxWithTimeout, cancelTimeout := context.WithTimeout(ctx, timeout)
	defer cancelTimeout()

	var nodes []*cdp.Node
	err := this.Run(ctxWithTimeout, chromedp.Nodes(sel, &nodes, chromedp.BySearch))
	if err != nil {
		return nil, erro.W("Failed to find button elements", err).KV("sel", sel)
	}
	return nodes, nil
}

func (this *WebDriver) WaitForTextOn(ctx context.Context, text string, aria string) error {
	sel := `//*[@role="` + aria + `" and contains(text(), "` + text + `")]`
	nodes, err := this.bySearch(ctx, sel)
	if err != nil {
		return err
	}
	if len(nodes) == 0 {
		return erro.N("No heading found with the specified text").KV("text", text).KV("sel", sel)
	}
	return nil
}

func (this *WebDriver) MustWaitForTextOn(ctx context.Context, text string, aria string) {
	err := this.WaitForTextOn(ctx, text, aria)
	lib.Poe(err)
}

func (this *WebDriver) FillStringFormField(ctx context.Context, formFieldName string, value string) error {
	sel := `[name="` + formFieldName + `"]`
	return this.Run(ctx, chromedp.SendKeys(sel, value, chromedp.ByQuery))
}

func (this *WebDriver) MustFillStringFormField(ctx context.Context, formFieldName string, value string) {
	err := this.FillStringFormField(ctx, formFieldName, value)
	lib.Poe(err)
}
