package parser

import (
	"fmt"
	"maps"
	"strconv"
	"strings"
	"time"

	"github.com/unkn0wn-root/resterm/internal/duration"
	"github.com/unkn0wn-root/resterm/internal/parser/directive/lex"
	"github.com/unkn0wn-root/resterm/internal/parser/directive/options"
	dscope "github.com/unkn0wn-root/resterm/internal/parser/directive/scope"
	dvalue "github.com/unkn0wn-root/resterm/internal/parser/directive/value"
	"github.com/unkn0wn-root/resterm/internal/restfile"
	"github.com/unkn0wn-root/resterm/internal/tracebudget"
	"github.com/unkn0wn-root/resterm/internal/vars"
)

func parseApplySpec(rest string, line int) (restfile.ApplySpec, error) {
	raw := strings.TrimSpace(rest)
	if after, ok := strings.CutPrefix(raw, "="); ok {
		raw = strings.TrimSpace(after)
	}
	if raw == "" {
		return restfile.ApplySpec{}, fmt.Errorf("@apply expression missing")
	}
	l := strings.ToLower(raw)
	if strings.HasPrefix(l, "use=") {
		us, err := parseApplyUses(raw)
		if err != nil {
			return restfile.ApplySpec{}, err
		}
		return restfile.ApplySpec{
			Uses: us,
			Line: line,
			Col:  1,
		}, nil
	}
	return restfile.ApplySpec{
		Expression: raw,
		Line:       line,
		Col:        1,
	}, nil
}

func parseApplyUses(raw string) ([]string, error) {
	ps := strings.Split(raw, ",")
	us := make([]string, 0, len(ps))
	for _, p := range ps {
		p = strings.TrimSpace(p)
		if p == "" {
			return nil, fmt.Errorf("@apply has an empty use token")
		}
		k, v, ok := strings.Cut(p, "=")
		if !ok {
			return nil, fmt.Errorf("@apply token %q must be use=<name>", p)
		}
		if !strings.EqualFold(strings.TrimSpace(k), "use") {
			return nil, fmt.Errorf("@apply token %q must be use=<name>", p)
		}
		n := strings.TrimSpace(lex.TrimQuotes(v))
		if !validPatchName(n) {
			return nil, fmt.Errorf("@apply use name %q is invalid", n)
		}
		us = append(us, n)
	}
	if len(us) == 0 {
		return nil, fmt.Errorf("@apply use= requires at least one profile name")
	}
	return us, nil
}

func parsePatchSpec(rest string, line int) (restfile.PatchProfile, error) {
	scTok, rem := lex.SplitFirst(rest)
	if scTok == "" {
		return restfile.PatchProfile{}, fmt.Errorf(
			"@patch requires '<scope> <name> <expression>'",
		)
	}
	sc, ok := parsePatchScope(scTok)
	if !ok {
		return restfile.PatchProfile{}, fmt.Errorf("@patch scope must be file or global")
	}
	n, rem := lex.SplitFirst(rem)
	n = strings.TrimSpace(n)
	if !validPatchName(n) {
		return restfile.PatchProfile{}, fmt.Errorf("@patch name %q is invalid", n)
	}
	ex := strings.TrimSpace(rem)
	if after, ok := strings.CutPrefix(ex, "="); ok {
		ex = strings.TrimSpace(after)
	}
	if ex == "" {
		return restfile.PatchProfile{}, fmt.Errorf("@patch %q expression missing", n)
	}
	return restfile.PatchProfile{
		Scope:      sc,
		Name:       n,
		Expression: ex,
		Line:       line,
		Col:        1,
	}, nil
}

func parsePatchScope(tok string) (restfile.PatchScope, bool) {
	switch strings.ToLower(strings.TrimSpace(tok)) {
	case "file":
		return restfile.PatchScopeFile, true
	case "global":
		return restfile.PatchScopeGlobal, true
	default:
		return 0, false
	}
}

func validPatchName(n string) bool {
	n = strings.TrimSpace(n)
	if n == "" {
		return false
	}
	for _, r := range n {
		if lex.IsIdentRune(r) {
			continue
		}
		if r == '-' || r == '.' {
			continue
		}
		return false
	}
	return true
}

func parseUseSpec(rest string, line int) (restfile.UseSpec, error) {
	f := lex.TokenizeFields(rest)
	n := len(f)
	switch n {
	case 0:
		return restfile.UseSpec{}, fmt.Errorf("@use requires a path")
	case 1:
		p := strings.TrimSpace(f[0])
		if p == "" {
			return restfile.UseSpec{}, fmt.Errorf("@use requires a non-empty path")
		}
		return restfile.UseSpec{Path: p, Line: line}, nil
	case 2:
		if strings.EqualFold(f[1], "as") {
			return restfile.UseSpec{}, fmt.Errorf("@use requires an alias after 'as'")
		}
		return restfile.UseSpec{}, fmt.Errorf("@use must be '<path>' or '<path> as <alias>'")
	case 3:
		if !strings.EqualFold(f[1], "as") {
			return restfile.UseSpec{}, fmt.Errorf("@use must use 'as' to define an alias")
		}
		p := strings.TrimSpace(f[0])
		a := strings.TrimSpace(f[2])
		if p == "" || a == "" {
			return restfile.UseSpec{}, fmt.Errorf("@use requires a non-empty path and alias")
		}
		if !lex.IsIdent(a) {
			return restfile.UseSpec{}, fmt.Errorf("@use alias %q is invalid", a)
		}
		return restfile.UseSpec{
			Path:  p,
			Alias: a,
			Line:  line,
		}, nil
	default:
		return restfile.UseSpec{}, fmt.Errorf("@use has too many tokens")
	}
}

func parseConditionSpec(rest string, line int, negate bool) (*restfile.ConditionSpec, error) {
	expr := strings.TrimSpace(rest)
	if expr == "" {
		return nil, fmt.Errorf("@when expression missing")
	}
	return &restfile.ConditionSpec{
		Expression: expr,
		Line:       line,
		Col:        1,
		Negate:     negate,
	}, nil
}

func parseForEachSpec(rest string, line int) (*restfile.ForEachSpec, error) {
	trimmed := strings.TrimSpace(rest)
	if trimmed == "" {
		return nil, fmt.Errorf("@for-each expression missing")
	}
	if idx := strings.LastIndex(trimmed, " as "); idx >= 0 {
		expr := strings.TrimSpace(trimmed[:idx])
		name := strings.TrimSpace(trimmed[idx+4:])
		if expr == "" || name == "" {
			return nil, fmt.Errorf("@for-each requires '<expr> as <name>'")
		}
		if !lex.IsIdent(name) {
			return nil, fmt.Errorf("@for-each name %q is invalid", name)
		}
		return &restfile.ForEachSpec{Expression: expr, Var: name, Line: line, Col: 1}, nil
	}
	if idx := strings.Index(trimmed, " in "); idx >= 0 {
		name := strings.TrimSpace(trimmed[:idx])
		expr := strings.TrimSpace(trimmed[idx+4:])
		if expr == "" || name == "" {
			return nil, fmt.Errorf("@for-each requires '<name> in <expr>'")
		}
		if !lex.IsIdent(name) {
			return nil, fmt.Errorf("@for-each name %q is invalid", name)
		}
		return &restfile.ForEachSpec{Expression: expr, Var: name, Line: line, Col: 1}, nil
	}
	return nil, fmt.Errorf("@for-each must use 'as' or 'in'")
}

func parseCaptureScope(token string) (restfile.CaptureScope, bool, bool) {
	lowered := strings.ToLower(strings.TrimSpace(token))
	secret := false
	if strings.HasSuffix(lowered, "-secret") {
		secret = true
		lowered = strings.TrimSuffix(lowered, "-secret")
	}
	switch lowered {
	case "request":
		return restfile.CaptureScopeRequest, secret, true
	case "file":
		return restfile.CaptureScopeFile, secret, true
	case "global":
		return restfile.CaptureScopeGlobal, secret, true
	default:
		return 0, false, false
	}
}

type authDirective struct {
	Scope   restfile.AuthScope
	Name    string
	Spec    *restfile.AuthSpec
	Disable bool
}

func parseAuthDirective(rest string) (authDirective, bool, error) {
	dir := authDirective{Scope: restfile.AuthScopeRequest}
	trimmed := strings.TrimSpace(rest)
	if trimmed == "" {
		return dir, false, nil
	}

	fields := lex.TokenizeFields(trimmed)
	if len(fields) == 0 {
		return dir, false, nil
	}

	explicitScope := false
	if scope, ok := parseAuthScope(fields[0]); ok {
		dir.Scope = scope
		explicitScope = true
		fields = fields[1:]
		if len(fields) == 0 {
			return dir, true, fmt.Errorf(
				"@auth %s scope requires an auth spec",
				restfile.AuthScopeLabel(scope),
			)
		}
	}

	if strings.EqualFold(fields[0], "none") {
		if dir.Scope != restfile.AuthScopeRequest {
			return dir, true, fmt.Errorf(
				"@auth %s scope does not support none",
				restfile.AuthScopeLabel(dir.Scope),
			)
		}
		if len(fields) != 1 {
			return dir, true, fmt.Errorf("@auth none does not accept additional tokens")
		}
		dir.Disable = true
		return dir, true, nil
	}

	spec := parseAuthSpec(strings.Join(fields, " "))
	if spec == nil {
		if explicitScope {
			return dir, true, fmt.Errorf(
				"@auth %s scope requires a valid auth spec",
				restfile.AuthScopeLabel(dir.Scope),
			)
		}
		return dir, false, nil
	}
	dir.Spec = spec
	return dir, true, nil
}

func parseAuthScope(token string) (restfile.AuthScope, bool) {
	return dscope.Parse(
		token,
		restfile.AuthScopeRequest,
		restfile.AuthScopeFile,
		restfile.AuthScopeGlobal,
	)
}

func parseAuthSpec(rest string) *restfile.AuthSpec {
	fields := lex.TokenizeFields(rest)
	if len(fields) == 0 {
		return nil
	}
	authType := strings.ToLower(fields[0])
	params := make(map[string]string)
	switch authType {
	case "basic":
		if len(fields) >= 3 {
			params["username"] = fields[1]
			params["password"] = strings.Join(fields[2:], " ")
		}
	case "bearer":
		if len(fields) >= 2 {
			params["token"] = strings.Join(fields[1:], " ")
		}
	case "apikey", "api-key":
		if len(fields) >= 4 {
			params["placement"] = strings.ToLower(fields[1])
			params["name"] = fields[2]
			params["value"] = strings.Join(fields[3:], " ")
		}
	case "oauth2":
		if len(fields) < 2 {
			return nil
		}
		maps.Copy(params, options.ParseFields(fields[1:]))
		if params["token_url"] == "" && params["cache_key"] == "" {
			return nil
		}
		if params["grant"] == "" {
			params["grant"] = "client_credentials"
		}
		if params["client_auth"] == "" {
			params["client_auth"] = "basic"
		}
	case "command":
		if len(fields) < 2 {
			return nil
		}
		maps.Copy(params, options.ParseFields(fields[1:]))
		if params["argv"] == "" && params["cache_key"] == "" {
			return nil
		}
	default:
		if len(fields) >= 2 {
			params["header"] = fields[0]
			params["value"] = strings.Join(fields[1:], " ")
			authType = "header"
		}
	}
	if len(params) == 0 {
		return nil
	}
	return &restfile.AuthSpec{Type: authType, Params: params}
}

func parseProfileSpec(rest string) *restfile.ProfileSpec {
	trimmed := strings.TrimSpace(rest)
	spec := &restfile.ProfileSpec{}

	if trimmed == "" {
		spec.Count = 10
		return spec
	}

	fields := lex.TokenizeFields(trimmed)
	params := options.ParseFields(fields)

	if spec.Count == 0 {
		if raw, ok := params["count"]; ok {
			if n, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil && n > 0 {
				spec.Count = n
			}
		}
	}

	if spec.Count == 0 && len(fields) == 1 && !strings.Contains(fields[0], "=") {
		if n, err := strconv.Atoi(fields[0]); err == nil && n > 0 {
			spec.Count = n
		}
	}

	if raw, ok := params["warmup"]; ok {
		if n, err := strconv.Atoi(strings.TrimSpace(raw)); err == nil && n >= 0 {
			spec.Warmup = n
		}
	}

	if raw, ok := params["delay"]; ok {
		if dur, ok := duration.Parse(raw); ok && dur >= 0 {
			spec.Delay = dur
		}
	}

	if spec.Count <= 0 {
		spec.Count = 10
	}
	if spec.Warmup < 0 {
		spec.Warmup = 0
	}
	return spec
}

func parseTraceSpec(rest string) *restfile.TraceSpec {
	spec := &restfile.TraceSpec{Enabled: true}
	trimmed := strings.TrimSpace(rest)
	if trimmed == "" {
		return spec
	}

	fields := lex.TokenizeFields(trimmed)
	for _, field := range fields {
		value := strings.TrimSpace(field)
		if value == "" {
			continue
		}
		lower := strings.ToLower(value)
		switch lower {
		case "off", "disable", "disabled", "false":
			spec.Enabled = false
			continue
		case "on", "enable", "enabled", "true":
			spec.Enabled = true
			continue
		}

		if parts := strings.SplitN(value, "<=", 2); len(parts) == 2 {
			name := tracebudget.NormalizePhase(parts[0])
			dur := parseDuration(parts[1])
			if name == "" || dur <= 0 {
				continue
			}
			if name == tracebudget.TotalPhase {
				spec.Budgets.Total = dur
				continue
			}
			if spec.Budgets.Phases == nil {
				spec.Budgets.Phases = make(map[string]time.Duration)
			}
			spec.Budgets.Phases[name] = dur
			continue
		}

		if idx := strings.Index(value, "="); idx != -1 {
			key := strings.ToLower(strings.TrimSpace(value[:idx]))
			val := strings.TrimSpace(value[idx+1:])
			switch key {
			case "enabled":
				if b, ok := dvalue.ParseBool(val); ok {
					spec.Enabled = b
				}
			case "total":
				if dur := parseDuration(val); dur > 0 {
					spec.Budgets.Total = dur
				}
			case "tolerance", "allowance", "grace":
				if dur := parseDuration(val); dur >= 0 {
					spec.Budgets.Tolerance = dur
				}
			default:
				dur := parseDuration(val)
				if dur <= 0 {
					continue
				}
				name := tracebudget.NormalizePhase(key)
				if name == "" {
					continue
				}
				if name == tracebudget.TotalPhase {
					spec.Budgets.Total = dur
					continue
				}
				if spec.Budgets.Phases == nil {
					spec.Budgets.Phases = make(map[string]time.Duration)
				}
				spec.Budgets.Phases[name] = dur
			}
		}
	}

	if len(spec.Budgets.Phases) == 0 {
		spec.Budgets.Phases = nil
	}
	return spec
}

func parseCompareDirective(rest string) (*restfile.CompareSpec, error) {
	fields := lex.TokenizeFields(rest)
	envs := make([]string, 0, len(fields))
	seen := make(map[string]struct{})
	var baseline string

	for _, field := range fields {
		value := strings.TrimSpace(field)
		if value == "" {
			continue
		}
		if idx := strings.Index(value, "="); idx != -1 {
			key := strings.ToLower(strings.TrimSpace(value[:idx]))
			val := strings.TrimSpace(value[idx+1:])
			switch key {
			case "base", "baseline", "primary", "ref":
				if val == "" {
					return nil, fmt.Errorf("@compare baseline cannot be empty")
				}
				if vars.IsReservedEnvironment(val) {
					return nil, fmt.Errorf(
						"@compare baseline %q is reserved for shared defaults",
						val,
					)
				}
				baseline = val
			default:
				return nil, fmt.Errorf("@compare unsupported option %q", key)
			}
			continue
		}
		if vars.IsReservedEnvironment(value) {
			return nil, fmt.Errorf("@compare environment %q is reserved for shared defaults", value)
		}
		lowered := strings.ToLower(value)
		if _, exists := seen[lowered]; exists {
			return nil, fmt.Errorf("@compare duplicate environment %q", value)
		}
		seen[lowered] = struct{}{}
		envs = append(envs, value)
	}

	if len(envs) < 2 {
		return nil, fmt.Errorf("@compare requires at least two environments")
	}

	if baseline == "" {
		baseline = envs[0]
	} else {
		match := ""
		for _, env := range envs {
			if strings.EqualFold(env, baseline) {
				match = env
				break
			}
		}
		if match == "" {
			return nil, fmt.Errorf(
				"@compare baseline %q must match one of the environments",
				baseline,
			)
		}
		baseline = match
	}

	return &restfile.CompareSpec{
		Environments: envs,
		Baseline:     baseline,
	}, nil
}

func parseDuration(value string) time.Duration {
	dur, ok := duration.Parse(value)
	if !ok {
		return 0
	}
	return dur
}
