"""Shared types and constants for the CLI."""

import os
import re
from dataclasses import dataclass, field
from typing import List, Optional

from api_clients.A2A.protocol import resolve_protocol_version

MAX_CONCURRENCY = 5
MAX_ATTEMPTS = 4  # Default attempts (initial attempt + 3 retries); see resolve_max_attempts
MAX_TURNS_PER_THREAD = 20
LONG_THREAD_WARNING_THRESHOLD = 10
DEFAULT_PASS_THRESHOLD = 3
# Code-only (non-LLM) custom evaluators default to a 0/1 (fail/pass) scale, so
# their pass threshold defaults to 1 rather than the 1-5 LLM default above.
DEFAULT_NON_LLM_PASS_THRESHOLD = 1

# ── Environment variable name constants ──────────────────────────────
ENV_AZURE_AI_OPENAI_ENDPOINT = "AZURE_AI_OPENAI_ENDPOINT"
ENV_AZURE_AI_API_KEY = "AZURE_AI_API_KEY"
ENV_AZURE_AI_API_VERSION = "AZURE_AI_API_VERSION"
ENV_AZURE_AI_MODEL_NAME = "AZURE_AI_MODEL_NAME"
ENV_AZURE_AI_PROJECT_ENDPOINT = "AZURE_AI_PROJECT_ENDPOINT"
ENV_TENANT_ID = "TENANT_ID"
ENV_WORK_IQ_A2A_ENDPOINT = "WORK_IQ_A2A_ENDPOINT"
ENV_WORK_IQ_A2A_CLIENT_ID = "WORK_IQ_A2A_CLIENT_ID"
ENV_WORK_IQ_A2A_SCOPES = "WORK_IQ_A2A_SCOPES"
ENV_WORK_IQ_GRAPH_ENDPOINT = "WORK_IQ_GRAPH_ENDPOINT"
ENV_WORK_IQ_GRAPH_SCOPES = "WORK_IQ_GRAPH_SCOPES"
ENV_REQUEST_MAX_ATTEMPTS = "WORKIQ_REQUEST_MAX_ATTEMPTS"
ENV_LOG_TRUNCATE = "RUNEVALS_LOG_TRUNCATE"
ENV_LOG_MAX_LENGTH = "RUNEVALS_LOG_MAX_LENGTH"

# Judge backend identifiers
JUDGE_BACKEND_AZURE = "azure"
JUDGE_BACKEND_GITHUB_COPILOT = "github-copilot"


def should_use_foundry_eval(
    model_name: Optional[str],
    project_endpoint: Optional[str],
) -> bool:
    """Decide whether LLM evaluators should run through Foundry cloud evaluation.

    Presence-based and driven entirely by ``.env``: Foundry cloud evaluation is
    used when both a Foundry project endpoint (``AZURE_AI_PROJECT_ENDPOINT``) and
    a judge model deployment (``AZURE_AI_MODEL_NAME``) are configured. This is
    required for gpt-5x / o-series judge models — whose Responses-API backing
    rejects the ``response_format`` parameter the local SDK evaluators send — and
    also works for gpt-4x. When no project endpoint is set, the local SDK
    evaluator path is used.
    """
    return bool(project_endpoint) and bool(model_name)


def resolve_max_attempts() -> int:
    """Resolve the max number of agent-request attempts (initial try + retries).

    Honors the ``WORKIQ_REQUEST_MAX_ATTEMPTS`` environment variable when it is
    set to an integer >= 1; otherwise falls back to the ``MAX_ATTEMPTS`` default.
    This is the single source of truth for the retry ceiling used by the
    evaluation runner, so operators can raise it for flaky/long-running agents
    without a code change.
    """
    raw = os.environ.get(ENV_REQUEST_MAX_ATTEMPTS)
    if raw is None or not raw.strip():
        return MAX_ATTEMPTS
    try:
        value = int(raw)
    except (TypeError, ValueError):
        _warn_invalid_max_attempts(raw)
        return MAX_ATTEMPTS
    if value < 1:
        _warn_invalid_max_attempts(raw)
        return MAX_ATTEMPTS
    return value


def _warn_invalid_max_attempts(raw: str) -> None:
    # Imported lazily to avoid a circular import at module load time
    # (cli_logging is a higher-level package than common).
    from cli_logging.cli_logger import emit_structured_log
    from cli_logging.logging_utils import Operation

    emit_structured_log(
        "warning",
        f"Ignoring invalid {ENV_REQUEST_MAX_ATTEMPTS}={raw!r}; "
        f"expected an integer greater than or equal to 1. "
        f"Using default {MAX_ATTEMPTS}.",
        Operation.SETUP,
    )


def pascal_case_to_title(eval_name: str) -> str:
    """Convert PascalCase evaluator name to space-separated display name.

    e.g., "ToolCallAccuracy" → "Tool Call Accuracy"
    """
    return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', eval_name)


# Canonical evaluator name constants
RELEVANCE = "Relevance"
COHERENCE = "Coherence"
GROUNDEDNESS = "Groundedness"
SIMILARITY = "Similarity"
TOOL_CALL_ACCURACY = "ToolCallAccuracy"
CITATIONS = "Citations"
EXACT_MATCH = "ExactMatch"
PARTIAL_MATCH = "PartialMatch"
RETRIEVAL_QUERY = "RetrievalQuery"
RETRIEVAL_RESULT = "RetrievalResult"

# Canonical set of all built-in evaluator names. Single source of truth for
# anywhere in the codebase that needs to enumerate built-ins (e.g. custom
# evaluator name-collision detection). When adding a new built-in, add the
# constant above and also add it to this set; the test suite asserts that
# every name registered in evaluator_resolver.EVALUATOR_REGISTRY also appears
# here, so drift is caught at test time.
BUILTIN_EVALUATOR_NAMES = frozenset({
    RELEVANCE,
    COHERENCE,
    GROUNDEDNESS,
    SIMILARITY,
    TOOL_CALL_ACCURACY,
    CITATIONS,
    EXACT_MATCH,
    PARTIAL_MATCH,
    RETRIEVAL_QUERY,
    RETRIEVAL_RESULT,
})

# Evaluation status constants — four-value enum used at the turn/item level
# AND the thread-level overall_status. See status_derivation.py for the
# canonical derivation and rollup rules.
STATUS_PASS = "pass"
STATUS_FAIL = "fail"
STATUS_PARTIAL = "partial"
STATUS_ERROR = "error"
# Internal-only sentinel — never appears in emitted output.
STATUS_UNKNOWN = "unknown"

# System defaults when no file-level or env-level defaults are configured
SYSTEM_DEFAULT_EVALUATORS = [
    RELEVANCE,
    COHERENCE,
]


# Mapping from evaluator name to the key used in evaluator output dicts
METRIC_IDS = {
    RELEVANCE: "relevance",
    COHERENCE: "coherence",
    GROUNDEDNESS: "groundedness",
    SIMILARITY: "similarity",
    TOOL_CALL_ACCURACY: "tool_call_accuracy",
    CITATIONS: "citations",
    EXACT_MATCH: "exact_match",
    PARTIAL_MATCH: "partial_match",
    RETRIEVAL_QUERY: "retrieval_query",
    RETRIEVAL_RESULT: "retrieval_result",
}


@dataclass
class RegistryEntry:
    type: str  # "llm", "tool", or "non-llm"
    default_threshold: Optional[float]


@dataclass(frozen=True)
class RunConfig:
    """Typed, immutable runtime configuration passed across module boundaries.

    Use ``RunConfig.from_namespace(args)`` to build from argparse output.
    Use ``dataclasses.replace(config, field=value)`` to derive new configs.
    """
    prompts: Optional[List[str]] = None
    expected: Optional[List[str]] = None
    prompts_file: Optional[str] = None
    evaluate_only: Optional[str] = None
    interactive: bool = False
    m365_agent_id: Optional[str] = None
    output: Optional[str] = None
    log_level: Optional[List[str]] = None
    effective_log_level: str = "info"
    signout: bool = False
    concurrency: int = MAX_CONCURRENCY
    azure_ai_auth_mode: Optional[str] = None
    account: Optional[str] = None
    judge_backend: str = "azure"
    #: A2A wire revision to speak. Resolved from the environment rather than a
    #: CLI flag: 0.3 is an emergency rollback lever, not a supported option.
    a2a_protocol_version: str = field(default_factory=resolve_protocol_version)

    @classmethod
    def from_namespace(cls, args) -> "RunConfig":
        """Build a RunConfig from an argparse.Namespace."""
        return cls(
            prompts=args.prompts,
            expected=args.expected,
            prompts_file=args.prompts_file,
            evaluate_only=getattr(args, "evaluate_only", None),
            interactive=args.interactive,
            m365_agent_id=args.m365_agent_id,
            output=args.output,
            log_level=args.log_level,
            signout=args.signout,
            concurrency=args.concurrency,
            azure_ai_auth_mode=args.azure_ai_auth_mode,
            account=args.account,
            judge_backend=args.judge_backend,
        )
