package signup

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/go-chi/chi/v5"
	"github.com/posthog/posthog-go"
	"tangled.org/core/appview/cloudflare"
	"tangled.org/core/appview/config"
	"tangled.org/core/appview/db"
	"tangled.org/core/appview/email"
	"tangled.org/core/appview/models"
	"tangled.org/core/appview/pages"
	"tangled.org/core/appview/state/userutil"
	"tangled.org/core/idresolver"
)

type Signup struct {
	config              *config.Config
	db                  *db.DB
	cf                  *cloudflare.Client
	posthog             posthog.Client
	idResolver          *idresolver.Resolver
	pages               *pages.Pages
	l                   *slog.Logger
	disallowedNicknames map[string]bool
}

func New(cfg *config.Config, database *db.DB, pc posthog.Client, idResolver *idresolver.Resolver, pages *pages.Pages, l *slog.Logger) *Signup {
	var cf *cloudflare.Client
	if cfg.Cloudflare.ApiToken != "" {
		var err error
		cf, err = cloudflare.New(cfg)
		if err != nil {
			l.Warn("failed to create cloudflare client, signup will be disabled", "error", err)
		}
	}

	disallowedNicknames := loadDisallowedNicknames(cfg.Core.DisallowedNicknamesFile, l)

	return &Signup{
		config:              cfg,
		db:                  database,
		posthog:             pc,
		idResolver:          idResolver,
		cf:                  cf,
		pages:               pages,
		l:                   l,
		disallowedNicknames: disallowedNicknames,
	}
}

func loadDisallowedNicknames(filepath string, logger *slog.Logger) map[string]bool {
	disallowed := make(map[string]bool)

	if filepath == "" {
		logger.Warn("no disallowed nicknames file configured")
		return disallowed
	}

	file, err := os.Open(filepath)
	if err != nil {
		logger.Warn("failed to open disallowed nicknames file", "file", filepath, "error", err)
		return disallowed
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	lineNum := 0
	for scanner.Scan() {
		lineNum++
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue // skip empty lines and comments
		}

		nickname := strings.ToLower(line)
		if userutil.IsValidSubdomain(nickname) {
			disallowed[nickname] = true
		} else {
			logger.Warn("invalid nickname format in disallowed nicknames file",
				"file", filepath, "line", lineNum, "nickname", nickname)
		}
	}

	if err := scanner.Err(); err != nil {
		logger.Error("error reading disallowed nicknames file", "file", filepath, "error", err)
	}

	logger.Info("loaded disallowed nicknames", "count", len(disallowed), "file", filepath)
	return disallowed
}

// isNicknameAllowed checks if a nickname is allowed (not in the disallowed list)
func (s *Signup) isNicknameAllowed(nickname string) bool {
	return !s.disallowedNicknames[strings.ToLower(nickname)]
}

func (s *Signup) Router() http.Handler {
	r := chi.NewRouter()
	r.Get("/", s.signup)
	r.Post("/", s.signup)
	r.Get("/complete", s.complete)
	r.Post("/complete", s.complete)

	return r
}

func (s *Signup) signup(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		emailId := r.URL.Query().Get("id")
		s.pages.Signup(w, pages.SignupParams{
			CloudflareSiteKey: s.config.Cloudflare.Turnstile.SiteKey,
			EmailId:           emailId,
		})
	case http.MethodPost:
		if s.cf == nil {
			http.Error(w, "signup is disabled", http.StatusFailedDependency)
			return
		}
		emailId := r.FormValue("email")
		cfToken := r.FormValue("cf-turnstile-response")

		noticeId := "signup-msg"

		if err := s.validateCaptcha(cfToken, r); err != nil {
			s.l.Warn("turnstile validation failed", "error", err, "email", emailId)
			s.pages.Notice(w, noticeId, "Captcha validation failed.")
			return
		}

		if !email.IsValidEmail(emailId) {
			s.pages.Notice(w, noticeId, "Invalid email address.")
			return
		}

		exists, err := db.CheckEmailExistsAtAll(s.db, emailId)
		if err != nil {
			s.l.Error("failed to check email existence", "error", err)
			s.pages.Notice(w, noticeId, "Failed to complete signup. Try again later.")
			return
		}
		if exists {
			s.pages.Notice(w, noticeId, "Email already exists.")
			return
		}

		code, err := s.inviteCodeRequest()
		if err != nil {
			s.l.Error("failed to create invite code", "error", err)
			s.pages.Notice(w, noticeId, "Failed to create invite code.")
			return
		}

		em := email.Email{
			APIKey:  s.config.Resend.ApiKey,
			From:    s.config.Resend.SentFrom,
			To:      emailId,
			Subject: "Verify your Tangled account",
			Text: `Copy and paste this code below to verify your account on Tangled.
		` + code,
			Html: `<p>Copy and paste this code below to verify your account on Tangled.</p>
<p><code>` + code + `</code></p>`,
		}

		err = email.SendEmail(em)
		if err != nil {
			s.l.Error("failed to send email", "error", err)
			s.pages.Notice(w, noticeId, "Failed to send email.")
			return
		}
		err = db.AddInflightSignup(s.db, models.InflightSignup{
			Email:      emailId,
			InviteCode: code,
		})
		if err != nil {
			s.l.Error("failed to add inflight signup", "error", err)
			s.pages.Notice(w, noticeId, "Failed to complete sign up. Try again later.")
			return
		}

		s.pages.HxRedirect(w, "/signup/complete")
	}
}

func (s *Signup) complete(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		s.pages.CompleteSignup(w)
	case http.MethodPost:
		username := r.FormValue("username")
		password := r.FormValue("password")
		code := r.FormValue("code")

		if !userutil.IsValidSubdomain(username) {
			s.pages.Notice(w, "signup-error", "Invalid username. Username must be 4–63 characters, lowercase letters, digits, or hyphens, and can't start or end with a hyphen.")
			return
		}

		if !s.isNicknameAllowed(username) {
			s.pages.Notice(w, "signup-error", "This username is not available. Please choose a different one.")
			return
		}

		email, err := db.GetEmailForCode(s.db, code)
		if err != nil {
			s.l.Error("failed to get email for code", "error", err)
			s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.")
			return
		}

		if s.cf == nil {
			s.l.Error("cloudflare client is nil", "error", "Cloudflare integration is not enabled in configuration")
			s.pages.Notice(w, "signup-error", "Account signup is currently disabled. DNS record creation is not available. Please contact support.")
			return
		}

		// Execute signup transactionally with rollback capability
		err = s.executeSignupTransaction(r.Context(), username, password, email, code, w)
		if err != nil {
			// Error already logged and notice already sent
			return
		}
	}
}

// executeSignupTransaction performs the signup process transactionally with rollback
func (s *Signup) executeSignupTransaction(ctx context.Context, username, password, email, code string, w http.ResponseWriter) error {
	// var recordID string
	var did string
	var emailAdded bool

	success := false
	defer func() {
		if !success {
			s.l.Info("rolling back signup transaction", "username", username, "did", did)

			// Rollback DNS record
			// if recordID != "" {
			// 	if err := s.cf.DeleteDNSRecord(ctx, recordID); err != nil {
			// 		s.l.Error("failed to rollback DNS record", "error", err, "recordID", recordID)
			// 	} else {
			// 		s.l.Info("successfully rolled back DNS record", "recordID", recordID)
			// 	}
			// }

			// Rollback PDS account
			if did != "" {
				if err := s.deleteAccountRequest(did); err != nil {
					s.l.Error("failed to rollback PDS account", "error", err, "did", did)
				} else {
					s.l.Info("successfully rolled back PDS account", "did", did)
				}
			}

			// Rollback email from database
			if emailAdded {
				if err := db.DeleteEmail(s.db, did, email); err != nil {
					s.l.Error("failed to rollback email from database", "error", err, "email", email)
				} else {
					s.l.Info("successfully rolled back email from database", "email", email)
				}
			}
		}
	}()

	// step 1: create account in PDS
	did, err := s.createAccountRequest(username, password, email, code)
	if err != nil {
		s.l.Error("failed to create account", "error", err)
		s.pages.Notice(w, "signup-error", err.Error())
		return err
	}

	// XXX: we have a wildcard *.tngl.sh record now
	// step 2: create DNS record with actual DID
	//	recordID, err = s.cf.CreateDNSRecord(ctx, cloudflare.DNSRecord{
	// 		Type:    "TXT",
	// 		Name:    "_atproto." + username,
	// 		Content: fmt.Sprintf(`"did=%s"`, did),
	// 		TTL:     6400,
	// 		Proxied: false,
	// 	})
	// 	if err != nil {
	// 		s.l.Error("failed to create DNS record", "error", err)
	// 		s.pages.Notice(w, "signup-error", "Failed to create DNS record for your handle. Please contact support.")
	// 		return err
	// 	}

	// step 3: add email to database
	err = db.AddEmail(s.db, models.Email{
		Did:      did,
		Address:  email,
		Verified: true,
		Primary:  true,
	})
	if err != nil {
		s.l.Error("failed to add email", "error", err)
		s.pages.Notice(w, "signup-error", "Failed to complete sign up. Try again later.")
		return err
	}
	emailAdded = true

	// step 4: auto-claim <username>.<pds-domain> for this user.
	// All signups through this flow receive a <username>.tngl.sh handle
	// (or whatever the configured PDS host is), so we claim the matching
	// sites subdomain on their behalf. This is the only way to obtain a
	// *.tngl.sh sites domain; it cannot be claimed manually via settings.
	pdsDomain := strings.TrimPrefix(s.config.Pds.Host, "https://")
	pdsDomain = strings.TrimPrefix(pdsDomain, "http://")
	autoClaimDomain := username + "." + pdsDomain
	if err := db.ClaimDomain(s.db, did, autoClaimDomain); err != nil {
		s.l.Warn("failed to auto-claim sites domain at signup",
			"domain", autoClaimDomain,
			"did", did,
			"error", err,
		)
	} else {
		s.l.Info("auto-claimed sites domain at signup", "domain", autoClaimDomain, "did", did)
	}

	// if we get here, we've successfully created the account and added the email
	success = true

	s.pages.NoticeHTMLWithClears(w, "signup-msg", fmt.Sprintf(`Account created successfully. You can now
		<a class="underline text-black dark:text-white" href="/login">login</a>
		with <code>%s.tngl.sh</code>.`, username), "signup-error")

	// clean up inflight signup asynchronously
	go func() {
		if err := db.DeleteInflightSignup(s.db, email); err != nil {
			s.l.Error("failed to delete inflight signup", "error", err)
		}
	}()

	return nil
}

type turnstileResponse struct {
	Success     bool     `json:"success"`
	ErrorCodes  []string `json:"error-codes,omitempty"`
	ChallengeTs string   `json:"challenge_ts,omitempty"`
	Hostname    string   `json:"hostname,omitempty"`
}

func (s *Signup) validateCaptcha(cfToken string, r *http.Request) error {
	if cfToken == "" {
		return errors.New("captcha token is empty")
	}

	if s.config.Cloudflare.Turnstile.SecretKey == "" {
		return errors.New("turnstile secret key not configured")
	}

	data := url.Values{}
	data.Set("secret", s.config.Cloudflare.Turnstile.SecretKey)
	data.Set("response", cfToken)

	// include the client IP if we have it
	if remoteIP := r.Header.Get("CF-Connecting-IP"); remoteIP != "" {
		data.Set("remoteip", remoteIP)
	} else if remoteIP := r.Header.Get("X-Forwarded-For"); remoteIP != "" {
		if ips := strings.Split(remoteIP, ","); len(ips) > 0 {
			data.Set("remoteip", strings.TrimSpace(ips[0]))
		}
	} else {
		data.Set("remoteip", r.RemoteAddr)
	}

	resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", data)
	if err != nil {
		return fmt.Errorf("failed to verify turnstile token: %w", err)
	}
	defer resp.Body.Close()

	var turnstileResp turnstileResponse
	if err := json.NewDecoder(resp.Body).Decode(&turnstileResp); err != nil {
		return fmt.Errorf("failed to decode turnstile response: %w", err)
	}

	if !turnstileResp.Success {
		s.l.Warn("turnstile validation failed", "error_codes", turnstileResp.ErrorCodes)
		return errors.New("turnstile validation failed")
	}

	return nil
}
