"""
Tier 1 faithfulness assessment — LLM-as-judge (semantic layer).

Complements the deterministic Tier 0 checks in faithfulness.py with
judgments only an LLM can make:
  - semantic faithfulness: paraphrased claims not supported by the context
  - response relevancy: AI turns that do not address what the caller said
  - tool intent: wrong tool for the caller's request, or argument values
    that contradict the caller's stated intent
  - compliance critics: per-profile yes/no conduct criteria
  - goal accuracy: was the caller's goal actually accomplished

Design:
  - ONE combined judge call per conversation (context + transcript are the
    dominant token cost; all metrics share them).
  - Runs post-call only, inside the same isolated hook as Tier 0. A judge
    failure degrades gracefully: Tier 0 results stand, error is recorded.
  - Prompt-building, parsing, and merging are pure functions (unit-testable
    without a network). Only run_judge() touches the LLM client.

Default judge model: qwen3.7-plus (stronger judgment + multilingual
transcripts); configurable via faithfulness_check.judge.model.
"""

import json

JUDGE_MODEL_DEFAULT = 'qwen3.7-plus'

# Generic criteria applied when the profile defines none.
DEFAULT_CRITERIA = [
    'The agent never told the caller an action was completed (message left, '
    'order placed, feedback submitted, call transferred) unless a matching '
    'function call actually happened in the call.',
]


def build_judge_prompt(messages, context_text, tool_events=None, criteria=None):
    """Build the single combined judge prompt. Pure function."""
    lines = []
    for i, m in enumerate(messages or []):
        speaker = (m.get('speaker') or '').lower()
        if speaker not in ('user', 'ai'):
            continue
        who = 'CALLER' if speaker == 'user' else 'AGENT'
        lines.append(f'[turn {i}] {who}: {m.get("text", "")}')
    transcript = '\n'.join(lines)

    tool_lines = []
    for ev in (tool_events or []):
        args = ev.get('arguments')
        try:
            args_s = json.dumps(args, ensure_ascii=False)
        except (TypeError, ValueError):
            args_s = str(args)
        res = ev.get('result')
        res_s = (str(res)[:300] if res is not None else '(no result)')
        tool_lines.append(f'- at turn {ev.get("msg_index", "?")}: '
                          f'{ev.get("name", "?")}({args_s}) -> {res_s}')
    tools_block = '\n'.join(tool_lines) if tool_lines else '(no tool calls)'

    crit = list(criteria or DEFAULT_CRITERIA)
    crit_block = '\n'.join(f'{i + 1}. {c}' for i, c in enumerate(crit))

    prompt = f"""You are a strict quality auditor for an AI phone agent. Judge the AGENT's turns in the call transcript against the CONTEXT (the only authoritative data the agent had) and the TOOL CALLS that actually happened.

CONTEXT (authoritative data given to the agent):
\"\"\"{context_text or '(empty)'}\"\"\"

TOOL CALLS that actually happened (with results):
{tools_block}

TRANSCRIPT:
\"\"\"{transcript}\"\"\"

COMPLIANCE CRITERIA to evaluate (answer pass/fail for each):
{crit_block}

Evaluate ONLY what can be judged from the material above. Respond with a valid JSON object, no other text:
{{
  "unsupported_claims": [
    {{"turn": <int>, "claim": "<the specific factual claim the agent made>", "reason": "<why the context/tools do not support it>"}}
  ],
  "relevancy_issues": [
    {{"turn": <int>, "issue": "<how the agent's reply failed to address the caller>"}}
  ],
  "tool_issues": [
    {{"turn": <int>, "tool": "<name>", "issue": "<wrong tool for the intent, or argument contradicting the caller's words>"}}
  ],
  "compliance": [
    {{"criterion": <int, 1-based index from the list above>, "pass": <bool>, "note": "<short justification>"}}
  ],
  "goal": {{"achieved": <bool or null if unclear>, "summary": "<one sentence: what the caller wanted and whether it was accomplished>"}}
}}

RULES:
- unsupported_claims: only FACTUAL claims (figures, statuses, policies, availability) that contradict or are absent from the context/tool results. Ignore pleasantries and numeric details (numbers are checked separately by another system) unless the meaning itself is wrong.
- relevancy_issues: only clear failures (ignored the question, answered something else). Not stylistic issues.
- tool_issues: judge tool CHOICE and argument MEANING against the caller's words. A tool that was appropriate and correctly parameterized is not an issue.
- compliance: every criterion must appear exactly once in the output.
- Be conservative: when in doubt, do not flag. Empty lists are the expected result for a good call.
- The transcript may mix English, Chinese, and Malay — judge meaning across languages."""
    return prompt


def parse_judge_output(raw):
    """Parse the judge LLM's raw text into a dict. Pure function.
    Raises ValueError on unusable output."""
    if not raw or not raw.strip():
        raise ValueError('empty judge output')
    text = raw.strip()
    if text.startswith('```'):
        text = text.strip('`')
        if text.startswith('json'):
            text = text[4:]
        text = text.strip()
    # Tolerate leading/trailing prose by extracting the outermost JSON object
    if not text.startswith('{'):
        start = text.find('{')
        end = text.rfind('}')
        if start == -1 or end == -1 or end <= start:
            raise ValueError('no JSON object in judge output')
        text = text[start:end + 1]
    out = json.loads(text)
    if not isinstance(out, dict):
        raise ValueError('judge output is not an object')
    # Normalize expected keys
    out.setdefault('unsupported_claims', [])
    out.setdefault('relevancy_issues', [])
    out.setdefault('tool_issues', [])
    out.setdefault('compliance', [])
    out.setdefault('goal', {'achieved': None, 'summary': ''})
    return out


def merge_judge_result(result, judge_out, model, criteria=None):
    """Merge parsed judge output into a Tier 0 result dict (mutates + returns).
    Pure function."""
    crit = list(criteria or DEFAULT_CRITERIA)
    flags = result.setdefault('flags', [])

    for c in judge_out.get('unsupported_claims') or []:
        flags.append({
            'turn': c.get('turn'),
            'claim_type': 'semantic',
            'spoken': str(c.get('claim', ''))[:200],
            'reason': str(c.get('reason', ''))[:200],
            'verdict': 'unsupported',
        })
    for c in judge_out.get('relevancy_issues') or []:
        flags.append({
            'turn': c.get('turn'),
            'claim_type': 'relevancy',
            'spoken': str(c.get('issue', ''))[:200],
            'verdict': 'violation',
        })
    for c in judge_out.get('tool_issues') or []:
        flags.append({
            'turn': c.get('turn'),
            'tool': c.get('tool'),
            'claim_type': 'tool_intent',
            'spoken': str(c.get('issue', ''))[:200],
            'verdict': 'violation',
        })
    compliance_out = []
    for c in judge_out.get('compliance') or []:
        try:
            idx = int(c.get('criterion', 0)) - 1
            text = crit[idx] if 0 <= idx < len(crit) else f'criterion {c.get("criterion")}'
        except (TypeError, ValueError):
            text = f'criterion {c.get("criterion")}'
        passed = bool(c.get('pass'))
        compliance_out.append({'criterion': text, 'pass': passed,
                               'note': str(c.get('note', ''))[:200]})
        if not passed:
            flags.append({
                'turn': None,
                'claim_type': 'compliance',
                'spoken': text[:200],
                'reason': str(c.get('note', ''))[:200],
                'verdict': 'violation',
            })

    goal = judge_out.get('goal') or {}
    result['judge'] = {
        'model': model,
        'compliance': compliance_out,
        'goal': {'achieved': goal.get('achieved'),
                 'summary': str(goal.get('summary', ''))[:300]},
    }
    result['flagged_count'] = len(flags)
    result['status'] = 'FAIL' if flags else 'PASS'
    return result


def run_judge(p, result, context_text, criteria=None, model=None):
    """Run the Tier 1 judge for a finished call and merge into `result`.

    Args:
        p: PipelineSession (uses p.llm, p.conversation_log, p._tool_events).
        result: the Tier 0 result dict to merge into.
        context_text: same resolved context used by Tier 0.
        criteria: optional list of compliance criterion strings.
        model: judge model name (default qwen3.7-plus).

    Never raises: on failure, records the error in result['judge'] and
    returns the (unmodified apart from that) Tier 0 result.
    """
    model = model or JUDGE_MODEL_DEFAULT
    try:
        prompt = build_judge_prompt(
            p.conversation_log, context_text,
            tool_events=getattr(p, '_tool_events', None),
            criteria=criteria)
        print(f'[Faithfulness-Judge] Judging via {model} '
              f'({len(p.conversation_log)} messages)...')
        response = p.llm.chat.completions.create(
            model=model,
            messages=[{'role': 'user', 'content': prompt}],
            temperature=0.1,
        )
        raw = response.choices[0].message.content
        judge_out = parse_judge_output(raw)
        merge_judge_result(result, judge_out, model, criteria=criteria)
        print(f'[Faithfulness-Judge] Done: status={result["status"]}, '
              f'{result["flagged_count"]} total flag(s)')
    except Exception as e:
        print(f'[Faithfulness-Judge] error (ignored): {e}')
        result['judge'] = {'model': model, 'error': str(e)[:200]}
    return result
