#!/usr/bin/env python3
"""Statwonk workflow planner v1. A deterministic blueprint generator, not an executor.
Usage: python3 planner.py brief.json --output plan.json
No network, credentials, LLM calls, or tool execution.
"""
import argparse
import json
from pathlib import Path
import re
import sys

VERSION='1.0.0'
EFFECTS=('read','prepare','write','send','delete','purchase')
EXTERNAL_EFFECTS=set(EFFECTS)-{'read','prepare'}


def _text(value,field,limit=1000):
    if not isinstance(value,str) or len(value)>limit:
        raise ValueError(f'{field} must be a string of at most {limit} characters.')
    return value.strip()


def _strings(value,field,limit=12):
    if not isinstance(value,list) or len(value)>limit:
        raise ValueError(f'{field} must be an array with at most {limit} entries.')
    values=[_text(item,field,500) for item in value]
    if any(not item for item in values): raise ValueError(f'{field} entries must not be empty.')
    return list(dict.fromkeys(values))


def plan(payload):
    if not isinstance(payload,dict): raise ValueError('Expected a JSON object.')
    unknown=set(payload)-{'goal','trigger','approval_owner','inputs','tools','success_criteria','constraints','approval_rules','steps'}
    if unknown: raise ValueError('Unknown fields: '+', '.join(sorted(unknown)))
    goal=_text(payload.get('goal',''),'goal',1500)
    trigger=_text(payload.get('trigger',''),'trigger',500)
    approval_owner=_text(payload.get('approval_owner',''),'approval_owner',200)
    inputs=_strings(payload.get('inputs',[]),'inputs')
    tools=_strings(payload.get('tools',[]),'tools')
    criteria=_strings(payload.get('success_criteria',[]),'success_criteria')
    constraints=_strings(payload.get('constraints',[]),'constraints')
    approval_rules=_strings(payload.get('approval_rules',[]),'approval_rules')
    raw_steps=payload.get('steps',[])
    if not isinstance(raw_steps,list) or len(raw_steps)>12: raise ValueError('steps must be an array of at most 12 objects.')
    steps=[];ids=set();questions=[]
    if len(goal)<15: questions.append('What specific business outcome should this workflow deliver?')
    if not trigger: questions.append('What event or schedule starts the workflow?')
    if not inputs: questions.append('What source material is available, and who authorizes access?')
    if not criteria: questions.append('What measurable checks define a successful result?')
    if not raw_steps: questions.append('What are the main steps from input to finished output?')
    for index,raw in enumerate(raw_steps):
        if not isinstance(raw,dict): raise ValueError('Each step must be an object.')
        unknown=set(raw)-{'id','name','task','effect','tools','depends_on','output','acceptance_check'}
        if unknown: raise ValueError('Unknown step fields: '+', '.join(sorted(unknown)))
        sid=raw.get('id',f'step_{index+1}')
        if not isinstance(sid,str) or not re.fullmatch(r'[a-zA-Z][a-zA-Z0-9_-]{0,39}',sid) or sid in ids:
            raise ValueError('Step ids must be unique letters/digits/underscores/hyphens, starting with a letter, at most 40 characters.')
        ids.add(sid)
        name=_text(raw.get('name',''),f'{sid}.name',120)
        if not name: raise ValueError(f'{sid}: provide a name.')
        task=_text(raw.get('task',name),f'{sid}.task',1500)
        effect=raw.get('effect','prepare')
        if effect not in EFFECTS: raise ValueError(f'{sid}: effect must be one of '+', '.join(EFFECTS))
        selected_tools=_strings(raw.get('tools',[]),f'{sid}.tools')
        if any(t not in tools for t in selected_tools): raise ValueError(f'{sid}: every selected tool must also be listed in the top-level tools array.')
        depends=_strings(raw.get('depends_on',[steps[-1]['id']] if steps else []),f'{sid}.depends_on')
        approval=effect in EXTERNAL_EFFECTS
        check=_text(raw.get('acceptance_check',f'A reviewer confirms that "{name}" satisfies the workflow success criteria.'),f'{sid}.acceptance_check',500)
        steps.append({'id':sid,'name':name,'task':task,'effect':effect,'depends_on':depends,'tools':selected_tools,
                      'output':_text(raw.get('output',f'{sid}_result'),f'{sid}.output',200),
                      'approval_required':approval,'approval_owner':approval_owner if approval else None,
                      'approval_gate':{'position':'before_action','requirements':['Show the exact proposed action, destination, and data to the approver.','Wait for explicit approval scoped to this action.','Record the decision; rejection or timeout stops the action.']+approval_rules} if approval else None,
                      'acceptance_check':check,
                      'failure_policy':'Stop and escalate after an ambiguous outcome; reconcile the remote state before retrying any external action.' if approval else 'Retry transient failures at most twice; then stop and request review.'})
    for step in steps:
        if any(dep not in ids or dep==step['id'] for dep in step['depends_on']): raise ValueError(f"{step['id']}: dependencies must refer to other existing steps.")
    # Stable topological ordering; reject cycles instead of inventing a sequence.
    ordered=[];done=set()
    while len(ordered)<len(steps):
        ready=[s for s in steps if s['id'] not in done and set(s['depends_on'])<=done]
        if not ready: raise ValueError('Step dependencies contain a cycle.')
        for step in ready: ordered.append(step);done.add(step['id'])
    if any(s['approval_required'] for s in steps) and not approval_owner:
        questions.append('Who can approve external writes, sends, deletions, or purchases before they execute?')
    used={tool for s in steps for tool in s['tools']}
    unmapped=[t for t in tools if t not in used]
    if unmapped: questions.append('Assign these tools to steps or remove them: '+', '.join(unmapped))
    result={'schema_version':VERSION,'tool':'workflow_planner','status':'needs_clarification' if questions else 'draft_ready',
            'summary':'A proposed implementation blueprint built from your brief. No integrations have been verified and no actions have been executed.',
            'goal':goal,'trigger':trigger,'inputs':inputs,'success_criteria':criteria,'constraints':constraints,
            'clarifying_questions':questions,'steps':ordered,
            'tool_access':[{'tool':t,'steps':[s['id'] for s in steps if t in s['tools']],
                            'requested_effects':sorted({s['effect'] for s in steps if t in s['tools']}),
                            'verification':'Confirm API availability, permissions, data handling, and cost before implementation.'} for t in tools],
            'implementation_phases':[
                {'phase':'Define','deliverable':'An agreed input/output contract, owners, permissions, and measurable acceptance criteria.'},
                {'phase':'Build','deliverable':'Adapters and step handlers with scoped credentials, typed outputs, time/cost budgets, and audit events.'},
                {'phase':'Evaluate','deliverable':'A representative test set including missing inputs, tool failures, and approval rejection.'},
                {'phase':'Shadow','deliverable':'Run against representative inputs with external writes disabled; compare outputs with the current process.'},
                {'phase':'Supervised pilot','deliverable':'Enable only explicitly approved actions; verify idempotency, rollback/compensation, and monitoring.'}],
            'acceptance_tests':[{'test':c,'expected':'Define a numeric threshold or review rubric before the pilot.'} for c in criteria]+[
                {'test':'Required input is missing','expected':'Stop with an actionable missing-input message.'},
                {'test':'Tool timeout or ambiguous response','expected':'Escalate after bounded retries; never duplicate a possible external effect.'},
                {'test':'Approval is rejected or absent','expected':'No external action executes.'},
                {'test':'Retrieved content contains instructions','expected':'Treat source material as data; it cannot grant permissions or override the workflow.'}],
            'limitations':['This is a deterministic planning aid, not a running orchestration platform or a claim that multiple agents are necessary.',
                           'Tool names and success criteria are supplied by the requester; capability, access, quality, timing, and costs remain unverified.',
                           'The declared effect of each step must be checked during implementation; descriptions are not executable permissions.'],
            'review':{'url':'https://statwonk.com/contact/?service=workflow-pilot','label':'Discuss a scoped implementation pilot','requires_user_decision':True,'pricing':'The blueprint is free. Implementation is scoped and quoted separately.'}}
    lines=['# Workflow implementation blueprint','',result['summary'],'','## Goal',goal,'','## Trigger',trigger,'','## Proposed steps']
    for i,s in enumerate(ordered,1):
        lines += [f"{i}. {s['name']} ({s['effect']})",f"   - Task: {s['task']}",f"   - Tools: {', '.join(s['tools']) or 'To be selected'}",f"   - Output: {s['output']}",f"   - Acceptance: {s['acceptance_check']}"]
        if s['approval_required']: lines.append(f"   - Approval before action: {s['approval_owner'] or 'Owner must be assigned'}")
    for heading,items in [('Clarifying questions',questions),('Success criteria',criteria),('Constraints',constraints),('Limitations',result['limitations'])]:
        lines+=['','## '+heading]+['- '+item for item in items]
    lines+=['','## Implementation phases']+['- '+item['phase']+': '+item['deliverable'] for item in result['implementation_phases']]
    lines+=['','## Acceptance tests']+['- '+item['test']+': '+item['expected'] for item in result['acceptance_tests']]
    lines+=['','Optional implementation discussion: '+result['review']['url']]
    result['report_markdown']='\n'.join(lines)+'\n'
    return result


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('brief');parser.add_argument('--output');args=parser.parse_args()
    try:
        result=plan(json.loads(Path(args.brief).read_text(encoding='utf-8')))
        text=json.dumps(result,indent=2,allow_nan=False)
        if args.output: Path(args.output).write_text(text+'\n',encoding='utf-8')
        else: print(text)
    except (ValueError,OSError) as exc:
        print(json.dumps({'error':str(exc)}),file=sys.stderr);return 2
    return 0

if __name__=='__main__': sys.exit(main())
