package token

import (
	"errors"
	"strings"
	"time"

	"github.com/google/uuid"
)

var (
	ErrUISessionInvalid  = errors.New("UI session is invalid")
	ErrUISessionMismatch = errors.New("UI session scope mismatch")
)

type UISessionDisposition string

const (
	UISessionDispositionCreated   UISessionDisposition = "created"
	UISessionDispositionUnchanged UISessionDisposition = "unchanged"
	UISessionDispositionRotated   UISessionDisposition = "rotated"
	UISessionDispositionReissued  UISessionDisposition = "reissued"
)

type UISessionMetadata struct {
	InstanceID       string
	WindowID         string
	LogicalSessionID string
	Generation       uint64
	ExpiresAt        time.Time
}

type UISessionIssue struct {
	Token       string
	Metadata    UISessionMetadata
	Disposition UISessionDisposition
}

const uiSessionRenewalLeadTime = 5 * time.Minute

// RenewUISession atomically commits a rotated token. The replaced bearer
// remains valid only until its original expiry so requests already dispatched
// by the renderer can finish during transport replacement.
func RenewUISession(token, instanceID, windowID string) (UISessionIssue, error) {
	token = strings.TrimSpace(token)
	instanceID = strings.TrimSpace(instanceID)
	windowID = strings.TrimSpace(windowID)
	if token == "" || instanceID == "" || windowID == "" {
		return UISessionIssue{}, ErrUISessionInvalid
	}
	now := time.Now()
	sessionMu.Lock()
	defer sessionMu.Unlock()
	pruneSessionsLocked(now)
	current, ok := sessionKeys[token]
	if !ok {
		return UISessionIssue{}, ErrUISessionInvalid
	}
	if current.InstanceID != instanceID || current.WindowID != windowID ||
		current.Generation == 0 {
		return UISessionIssue{}, ErrUISessionMismatch
	}
	logicalID := logicalSessionID(current, token)
	for _, record := range sessionKeys {
		if record.LogicalSessionID == logicalID &&
			record.Generation > current.Generation {
			return UISessionIssue{}, ErrUISessionInvalid
		}
	}
	next := UISessionIssue{
		Token: uuid.New().String(),
		Metadata: UISessionMetadata{
			InstanceID:       instanceID,
			WindowID:         windowID,
			LogicalSessionID: logicalID,
			Generation:       current.Generation + 1,
			ExpiresAt:        now.Add(uiSessionMaxDuration),
		},
		Disposition: UISessionDispositionRotated,
	}
	storeUISessionLocked(next, now)
	extendLogicalSessionLeaseLocked(logicalID, next.Metadata.ExpiresAt)
	enforceSessionLimitLocked()
	return next, nil
}

// EnsureUISession reconciles the renderer lease for one trusted Electron
// window. It is safe to retry after response loss: a live lease with sufficient
// remaining lifetime is returned unchanged, while an expired lease is reissued
// for the same stable logical UI session.
func EnsureUISession(instanceID, windowID string) (UISessionIssue, error) {
	instanceID = strings.TrimSpace(instanceID)
	windowID = strings.TrimSpace(windowID)
	if instanceID == "" || windowID == "" {
		return UISessionIssue{}, ErrUISessionInvalid
	}
	now := time.Now()
	sessionMu.Lock()
	defer sessionMu.Unlock()
	pruneSessionsLocked(now)

	var currentToken string
	var current sessionRecord
	for token, record := range sessionKeys {
		if record.InstanceID != instanceID || record.WindowID != windowID {
			continue
		}
		if currentToken == "" || record.Generation > current.Generation {
			currentToken = token
			current = record
		}
	}

	if currentToken == "" {
		issue := newUISessionIssue(instanceID, windowID, uuid.New().String(), 1, now)
		issue.Disposition = UISessionDispositionCreated
		storeUISessionLocked(issue, now)
		enforceSessionLimitLocked()
		return issue, nil
	}
	if current.ExpiresAt.After(now.Add(uiSessionRenewalLeadTime)) {
		return issueFromRecord(currentToken, current, UISessionDispositionUnchanged), nil
	}

	disposition := UISessionDispositionRotated
	if !current.ExpiresAt.After(now) {
		disposition = UISessionDispositionReissued
	}
	next := newUISessionIssue(
		instanceID,
		windowID,
		logicalSessionID(current, currentToken),
		current.Generation+1,
		now,
	)
	next.Disposition = disposition
	storeUISessionLocked(next, now)
	extendLogicalSessionLeaseLocked(
		next.Metadata.LogicalSessionID,
		next.Metadata.ExpiresAt,
	)
	pruneSessionsLocked(now)
	enforceSessionLimitLocked()
	return next, nil
}

func UISessionInfo(token string) (UISessionMetadata, bool) {
	now := time.Now()
	sessionMu.Lock()
	defer sessionMu.Unlock()
	pruneSessionsLocked(now)
	record, ok := sessionKeys[strings.TrimSpace(token)]
	if !ok || record.InstanceID == "" || record.WindowID == "" || record.Generation == 0 {
		return UISessionMetadata{}, false
	}
	return UISessionMetadata{
		InstanceID:       record.InstanceID,
		WindowID:         record.WindowID,
		LogicalSessionID: logicalSessionID(record, token),
		Generation:       record.Generation,
		ExpiresAt:        record.ExpiresAt,
	}, true
}

func RevokeUISessionWindow(instanceID, windowID string) []UISessionIssue {
	instanceID = strings.TrimSpace(instanceID)
	windowID = strings.TrimSpace(windowID)
	if instanceID == "" || windowID == "" {
		return nil
	}
	sessionMu.Lock()
	defer sessionMu.Unlock()
	return revokeUISessionsLocked(func(record sessionRecord) bool {
		return record.InstanceID == instanceID && record.WindowID == windowID
	})
}

func RevokeUISessionInstance(instanceID string) []UISessionIssue {
	instanceID = strings.TrimSpace(instanceID)
	if instanceID == "" {
		return nil
	}
	sessionMu.Lock()
	defer sessionMu.Unlock()
	return revokeUISessionsLocked(func(record sessionRecord) bool {
		return record.InstanceID == instanceID
	})
}

func revokeUISessionsLocked(matches func(sessionRecord) bool) []UISessionIssue {
	var revoked []UISessionIssue
	for token, record := range sessionKeys {
		if !matches(record) {
			continue
		}
		revoked = append(revoked, UISessionIssue{
			Token: token,
			Metadata: UISessionMetadata{
				InstanceID:       record.InstanceID,
				WindowID:         record.WindowID,
				LogicalSessionID: logicalSessionID(record, token),
				Generation:       record.Generation,
				ExpiresAt:        logicalSessionExpiresAt(record),
			},
		})
		delete(sessionKeys, token)
	}
	return revoked
}

func newUISessionIssue(instanceID, windowID, logicalSessionID string, generation uint64, now time.Time) UISessionIssue {
	return UISessionIssue{
		Token: uuid.New().String(),
		Metadata: UISessionMetadata{
			InstanceID:       instanceID,
			WindowID:         windowID,
			LogicalSessionID: logicalSessionID,
			Generation:       generation,
			ExpiresAt:        now.Add(uiSessionMaxDuration),
		},
	}
}

func storeUISessionLocked(issue UISessionIssue, now time.Time) {
	sessionKeys[issue.Token] = sessionRecord{
		CreatedAt:        now,
		ExpiresAt:        issue.Metadata.ExpiresAt,
		LogicalExpiresAt: issue.Metadata.ExpiresAt,
		InstanceID:       issue.Metadata.InstanceID,
		WindowID:         issue.Metadata.WindowID,
		LogicalSessionID: issue.Metadata.LogicalSessionID,
		Generation:       issue.Metadata.Generation,
	}
}

func extendLogicalSessionLeaseLocked(logicalSessionID string, expiresAt time.Time) {
	for token, record := range sessionKeys {
		if record.LogicalSessionID != logicalSessionID {
			continue
		}
		record.LogicalExpiresAt = expiresAt
		sessionKeys[token] = record
	}
}

func issueFromRecord(token string, record sessionRecord, disposition UISessionDisposition) UISessionIssue {
	return UISessionIssue{
		Token: token,
		Metadata: UISessionMetadata{
			InstanceID:       record.InstanceID,
			WindowID:         record.WindowID,
			LogicalSessionID: logicalSessionID(record, token),
			Generation:       record.Generation,
			ExpiresAt:        record.ExpiresAt,
		},
		Disposition: disposition,
	}
}

func logicalSessionID(record sessionRecord, fallback string) string {
	if logicalSessionID := strings.TrimSpace(record.LogicalSessionID); logicalSessionID != "" {
		return logicalSessionID
	}
	return fallback
}
