"""ASCII lowercase tel URI subset. No dialing/lookup/number allocation validation."""
import re,json,platform
class Unsupported(ValueError):pass
def number(s,global_number=False):
 pattern=r'\+[0-9().-]+' if global_number else r'[0-9().-]+'
 if not re.fullmatch(pattern,s) or not re.search('[0-9]',s):raise Unsupported('unsupported number grammar')
 return re.sub('[().-]','',s)
def parse(uri):
 if not isinstance(uri,str) or len(uri)>256 or not uri.isascii() or not uri.startswith('tel:'):raise Unsupported('ASCII lowercase scheme subset')
 items=uri[4:].split(';');num=number(items[0],items[0].startswith('+'));params={};order=[]
 for p in items[1:]:
  if p.count('=')!=1:raise Unsupported('parameter grammar')
  key,val=p.split('=')
  if key in params:raise ValueError('duplicate parameter')
  params[key]=val;order.append(key)
 if 'ext'in params and 'isub'in params:raise ValueError('conflicting extension and subaddress')
 if set(params)-{'ext','phone-context'}:raise Unsupported('parameter outside supported subset')
 if 'ext'in params:params['ext']=number(params['ext'])
 if num.startswith('+'):
  if 'phone-context'in params:raise Unsupported('global context outside subset')
 else:
  if not params.get('phone-context'):raise ValueError('local context required')
  c=params['phone-context']
  if c.startswith('+'):params['phone-context']=number(c,True)
  elif not re.fullmatch(r'[a-z]+(?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z]+(?:[a-z0-9-]*[a-z0-9])?)+',c):raise Unsupported('context domain subset')
 canonical_order=[k for k in ['ext','phone-context']if k in params]
 if order!=canonical_order:raise Unsupported('parameter order outside subset')
 return {'number':num,'global':num.startswith('+'),'extension':params.get('ext'),'context':params.get('phone-context')}
def serialize(r):
 out='tel:'+r['number']
 if r['extension']is not None:out+=';ext='+r['extension']
 if r['context']is not None:out+=';phone-context='+r['context']
 return out
def demo():
 examples=['tel:+1-201-555-0123','tel:+1-201-555-0123;ext=0042','tel:0042;phone-context=pbx.example.test','tel:0042;ext=007;phone-context=pbx.example.test','tel:911;phone-context=+1']
 rows=[]
 for s in examples:
  r=parse(s);canonical=serialize(r);assert parse(canonical)==r;rows.append({'input':s,'parsed':r,'subset_serialized':canonical})
 assert rows[1]['parsed']['extension']=='0042' and rows[4]['parsed']['number']=='911' and rows[4]['parsed']['context']=='+1'
 rejected=[]
 for s in ['tel:0042','tel:+1-201-555-0123;ext=1;ext=2','tel:0042;ext=1;isub=2;phone-context=pbx.example.test','tel:0042;phone-context=pbx.example.test;ext=1','tel:+1-201-555-0123;foo=bar','tel:0042;phone-context=pbx.example.test;m-test=x','tel:۱۲۳;phone-context=pbx.example.test','tel:+1 201 555 0123','TEL:+12015550123']:
  try:parse(s)
  except ValueError as e:rejected.append({'input':s,'kind':'unsupported_subset'if isinstance(e,Unsupported)else'declared_constraint','reason':str(e)})
  else:raise AssertionError(s)
 return {'runtime':platform.python_version(),'examples':rows,'rejected':rejected,'context_concatenation_performed':False,'full_rfc_equivalence_or_allocation_validation':False,'numbers_are_rfc_or_explicit_fictional_examples_not_customers':True,'dial_or_lookup_performed':False,'all_assertions_passed':True}
if __name__=='__main__':print(json.dumps(demo(),ensure_ascii=True,indent=2))
