"""Conservative iterative application-form completion. Never submits or guesses declarations."""
import re, time, os
from visual_controls import complete_dropdowns

FIELD_PATTERNS={
 'email':r'\be-?mail\b', 'phone':r'\b(phone|mobile|telephone|contact number)\b',
 'linkedin':r'linkedin', 'portfolio':r'portfolio|github|personal website',
 'first_name':r'\b(first name|given name|given name\(s\))\b', 'last_name':r'\b(last name|surname|family name)\b',
 'full_name':r'\b(full name|candidate name)\b',
 'address_line1':r'^(?:address(?: line)?[ -]?(?:1|one)|street address(?: line 1)?|address|address 1|street 1)$',
 'address_line2':r'^(?:address(?: line)?[ -]?(?:2|two)|apartment|suite|unit|street 2)$',
 'city':r'^(?:city|town|city / town|city or town)$',
 'state':r'^(?:state|state / province|state/province|province|region)$',
 'postal_code':r'^(?:postal(?: / pin)? code|postal code|zip(?: / postal)? code|zip code|pin code|pincode|postcode|postal / zip code)$',
}

def _label(el):
 """Resolve the actual question label, never an example-value placeholder."""
 return el.evaluate(r'''e=>{
 const clean=s=>(s||'').replace(/\s+/g,' ').replace(/\*/g,'').trim();
 const byId=id=>document.getElementById(id)?.innerText||'';
 let t=clean([...(e.labels||[])].map(x=>x.innerText).join(' '));
 if(!t){const id=e.getAttribute('aria-labelledby');if(id)t=clean(id.split(/\s+/).map(byId).join(' '));}
 if(!t)t=clean(e.getAttribute('aria-label'));
 if(!t&&e.id){try{t=clean(document.querySelector('label[for="'+CSS.escape(e.id)+'"]')?.innerText)}catch(_){}}
 if(!t)t=clean(e.closest('label')?.innerText);
 // Applied Materials wraps labels and inputs in neighboring React components.
 // Search nearest ancestor first, ignoring placeholder text and section headings.
 if(!t){let p=e.parentElement;for(let d=0;p&&d<5;d++,p=p.parentElement){
   let candidates=[...p.querySelectorAll('label,[class*="label"],[class*="Label"],legend')]
     .filter(x=>!x.contains(e)&&!x.querySelector('input,textarea,select'));
   if(candidates.length===1){let candidate=clean(candidates[0].innerText);
     if(candidate&&candidate.length<90){t=candidate;break;}}
   if(p.querySelectorAll('input:not([type="hidden"]),textarea,select').length>1)break;
 }}
 if(!t)t=clean(e.name||e.id);
 return t.slice(0,140);
 }''')

def _profile_values(profile):
 name=str(profile.get('full_name','')).strip().split()
 return {**profile,'first_name':name[0] if name else '', 'last_name':' '.join(name[1:]) if len(name)>1 else ''}

def _country(profile):
 # Only use a country explicitly present in approved location, not a guessed nationality.
 approved=str(profile.get('country','')).strip()
 if approved:return approved
 loc=str(profile.get('location','')).strip()
 if re.search(r'\b(india|bharat)\b',loc,re.I):return 'India'
 return ''

def _snapshot_missing(page,resume):
 missing=[]; seen=set()
 for frame in page.frames:
  try:
   controls=frame.locator('input[required],select[required],textarea[required],[aria-required="true"]')
   for i in range(min(controls.count(),200)):
    el=controls.nth(i)
    if not el.is_visible() or not el.is_enabled():continue
    bad=el.evaluate(r'''e=>{let t=(e.type||'').toLowerCase();if(t==='checkbox'||t==='radio'){let group=e.name?document.querySelectorAll('input[name="'+CSS.escape(e.name)+'"]'):[e];return ![...group].some(x=>x.checked)}if(t==='file')return !(e.files?.length);if(e.tagName==='SELECT')return !e.value||e.selectedIndex<0;return !String(e.value||'').trim()}''')
    if bad:
     label=_label(el) or 'Unlabeled required control'
     if label not in seen:seen.add(label);missing.append({'field':label,'result':'needs input — required control unresolved'})
  except Exception:continue
 return missing

def _upload(page,resume,report):
 if not resume or not os.path.isfile(resume):return {'field':'Resume','result':'needs input — no approved resume available'}
 options=[]
 for frame in page.frames:
  try:
   files=frame.locator('input[type="file"]')
   for i in range(min(files.count(),20)):
    el=files.nth(i)
    if not el.is_enabled():continue
    accept=(el.get_attribute('accept') or '').lower()
    if accept and not any(x in accept for x in ('.pdf','.doc','.docx','word','*')):continue
    options.append(el)
  except Exception:pass
 if len(options)!=1:return {'field':'Resume','result':'needs review — '+('no accessible upload input' if not options else 'multiple upload inputs')}
 el=options[0]
 try:
  # Avoid reuploading if an attachment is already present.
  existing=el.evaluate('e=>e.files?.length||0')
  if not existing:el.set_input_files(resume,timeout=12000);report('running','Selected approved resume in employer upload control.')
  time.sleep(.8)
  attached=el.evaluate('e=>({count:e.files?.length||0,name:e.files?.[0]?.name||""})')
  if attached['count']==1:
   return {'field':'Resume','result':'attached locally — employer acceptance requires review'}
  # Some React applications replace the file input after uploading; do not assert failure.
  return {'field':'Resume','result':'needs review — upload input reset; check displayed attachment'}
 except Exception as exc:return {'field':'Resume','result':'needs review — upload error: '+str(exc)[:90]}

def complete_form(page,profile,resume,report,max_passes=4):
 """Returns a unique final audit; actions are bounded, candidate-sourced, and non-submitting."""
 approved=_profile_values(profile);country=_country(profile)
 verified={};notes={};previous=None
 # Employer forms may mount address controls only AFTER country is chosen.
 # Resolve the country and dial code first, wait for React rerender, then
 # scan newly mounted address fields. Do not infer any unapproved value.
 dropdown_verified,dropdown_issues=complete_dropdowns(page,profile,report)
 page.wait_for_timeout(900)
 for attempt in range(max_passes):
  changes=0
  for frame in page.frames:
   try:
    fields=frame.locator('input:not([type="hidden"]):not([type="file"]),textarea,select,[role="combobox"]')
    for i in range(min(fields.count(),220)):
     el=fields.nth(i)
     if not el.is_visible() or not el.is_enabled():continue
     typ=(el.get_attribute('type') or '').lower()
     if typ in ('submit','button','checkbox','radio','password','date','number','file'):continue
     label=_label(el).strip().rstrip('*').strip()
     if not label:continue
     tag=el.evaluate('e=>e.tagName.toLowerCase()')
     role=el.get_attribute('role') or ''
     key=None;value=''
     if re.search(r'\bcountry code\b|dial(?:ing)? code',label,re.I):
      # Phone dial codes are not assumed from country or phone number.
      continue
     if re.fullmatch(r'(?:country|country of residence|current country)',label,re.I):
      key='country';value=country
     else:
      # Identity fields are mapped by the full visible label, never by a
      # nearby container containing multiple labels (e.g. contact section).
      normalized=re.sub(r'\s+',' ',label).strip().lower().rstrip(':')
      identity={'english first name':'first_name','first name':'first_name',
                'given name':'first_name','english last name':'last_name',
                'last name':'last_name','family name':'last_name',
                'surname':'last_name','email':'email','e-mail':'email',
                'email address':'email','e-mail address':'email'}
      if normalized in identity:
       key=identity[normalized];value=str(approved.get(key,'')).strip()
      else:
       matches=[k for k,p in FIELD_PATTERNS.items() if re.search(p,label,re.I)]
       if len(matches)==1:key=matches[0];value=str(approved.get(key,'')).strip()
     if not key or not value:continue
     # Address fields must be exact candidate-approved profile values; never infer them from location.
     if key in ('address_line1','address_line2','city','state','postal_code') and not str(profile.get(key,'')).strip():continue
     if key=='email' and (re.fullmatch(r'[^@]+@example\.(?:com|org|net)',value,re.I) or not re.fullmatch(r'[^\s@]+@[^\s@]+\.[^\s@]+',value)):
      notes[label]='needs input — valid approved email required';continue
     try:
      if tag=='select':
       current=el.input_value()
       if current and not re.search(r'^(select|choose|please select)$',current,re.I):
        continue
       opts=el.locator('option')
       found=[]
       for j in range(min(opts.count(),120)):
        opt=opts.nth(j);txt=opt.inner_text().strip()
        if txt.casefold()==value.casefold():found.append(opt.get_attribute('value'))
       if len(found)!=1:continue
       el.select_option(value=found[0],timeout=4000)
       ok=el.input_value()==found[0]
      elif role=='combobox':
       # Avoid clicking unknown JS dropdowns; their options may include declarations.
       continue
      else:
       current=el.input_value().strip()
       # The candidate-approved profile is authoritative for identity fields.
       # Correct wrong prefilled values and example addresses, but do not
       # overwrite unrelated user-provided answers.
       if current and current != value and key not in ('first_name','last_name','email'):
        notes[label]='needs review — existing value differs from approved profile'
        continue
       if current != value:
        el.fill(value,timeout=4000)
        el.dispatch_event('input')
        el.dispatch_event('change')
        el.blur(timeout=2000)
        page.wait_for_timeout(250)
       ok=el.input_value().strip()==value
      if ok:
       verified[label]='verified';notes.pop(label,None)
       if current != value if tag not in ('select',) and role!='combobox' else True:changes+=1
       report('running','Verified '+label[:70])
      else:notes[label]='needs review — value not retained'
     except Exception as e:notes[label]='needs review — interaction failed: '+str(e)[:65]
   except Exception as e:report('running','Form pass warning: '+str(e)[:90])
  # Selecting a native country control may also reveal dependent fields.
  # Re-scan dropdowns before the next text pass, not only after all passes.
  if attempt == 0:
   extra_verified,extra_issues=complete_dropdowns(page,profile,report)
   dropdown_verified.extend(extra_verified)
   dropdown_issues.extend(extra_issues)
  page.wait_for_timeout(800)
  fingerprint=tuple(sorted(verified))
  if changes==0 or fingerprint==previous:break
  previous=fingerprint
 # Final pass to catch options loaded after an address/state update.
 final_verified,final_issues=complete_dropdowns(page,profile,report)
 dropdown_verified.extend(final_verified)
 dropdown_issues.extend(final_issues)
 # Dedupe repeated verification records; success takes precedence over an
 # earlier transient failure when a dropdown was not yet mounted.
 successful={r['field'] for r in dropdown_verified}
 dropdown_verified=list({r['field']:r for r in dropdown_verified}.values())
 dropdown_issues=list({r['field']:r for r in dropdown_issues if r['field'] not in successful}.values())
 # React/ATS pages may rerender after the last fill. Audit all identity
 # controls by their *own* associated labels, then replace any stale success.
 # A duplicate or ambiguous label must never be treated as verified.
 page.wait_for_timeout(1200)
 identity_keys={'english first name':'first_name','first name':'first_name',
                'english last name':'last_name','last name':'last_name',
                'email':'email','email address':'email','e-mail':'email',
                'phone number':'phone','phone':'phone','mobile number':'phone'}
 observed={}
 for frame in page.frames:
  try:
   fields=frame.locator('input:not([type="hidden"]):not([type="file"])')
   for i in range(min(fields.count(),220)):
    el=fields.nth(i)
    if not el.is_visible() or not el.is_enabled():continue
    label=_label(el).strip().rstrip('*').strip()
    norm=re.sub(r'\s+',' ',label).strip().lower().rstrip(':')
    key=identity_keys.get(norm)
    # Some ATS phone controls have no label, only a specific placeholder/id.
    if not key:
     placeholder=(el.get_attribute('placeholder') or '').strip().lower()
     ident=(el.get_attribute('id') or '').lower()
     if placeholder=='phone number' or re.search(r'contact_information_phone$',ident):
      key='phone';label='Phone Number'
    if not key:continue
    observed.setdefault(key,[]).append((label,el.input_value().strip()))
  except Exception as exc:report('running','Identity audit warning: '+str(exc)[:80])
 # Repair a consent-triggered React rerender once, using exact candidate-approved
 # identity values. Re-query the live node for every field and check after blur.
 # Never touch declarations, demographic questions, or submit controls.
 for key in ('first_name','last_name','email','phone'):
  expected=str(approved.get(key,'')).strip()
  matches=observed.get(key,[])
  if not expected or len(matches)!=1 or matches[0][1]==expected:continue
  for frame in page.frames:
   try:
    inputs=frame.locator('input:not([type="hidden"]):not([type="file"])')
    for idx in range(min(inputs.count(),220)):
     el=inputs.nth(idx)
     if not el.is_visible() or not el.is_enabled():continue
     lbl=_label(el).strip().rstrip('*').strip()
     norm=re.sub(r'\s+',' ',lbl).strip().lower().rstrip(':')
     actual_key=identity_keys.get(norm)
     if not actual_key and key=='phone':
      ph=(el.get_attribute('placeholder') or '').strip().lower()
      ident=(el.get_attribute('id') or '').lower()
      if ph=='phone number' or re.search(r'contact_information_phone$',ident):actual_key='phone'
     if actual_key!=key:continue
     # Only one candidate was observed in the audit; never guess a duplicate.
     el.fill(expected,timeout=4000)
     el.blur(timeout=2000)
     page.wait_for_timeout(450)
     report('running','Restored candidate-approved '+key+' after form update.')
     break
   except Exception as exc:
    report('running','Identity restoration warning: '+str(exc)[:80])
 # Re-read after restoration, not from the pre-repair snapshot.
 page.wait_for_timeout(700)
 observed={}
 for frame in page.frames:
  try:
   fields=frame.locator('input:not([type="hidden"]):not([type="file"])')
   for i in range(min(fields.count(),220)):
    el=fields.nth(i)
    if not el.is_visible() or not el.is_enabled():continue
    label=_label(el).strip().rstrip('*').strip()
    norm=re.sub(r'\s+',' ',label).strip().lower().rstrip(':')
    key=identity_keys.get(norm)
    if not key:
     ph=(el.get_attribute('placeholder') or '').strip().lower()
     ident=(el.get_attribute('id') or '').lower()
     if ph=='phone number' or re.search(r'contact_information_phone$',ident):key='phone';label='Phone Number'
    if key:observed.setdefault(key,[]).append((label,el.input_value().strip()))
  except Exception as exc:report('running','Post-restoration audit warning: '+str(exc)[:80])
 # Discard earlier optimistic verification; it may refer to a replaced node.
 for key in identity_keys.values():
  for label in list(verified):
   if identity_keys.get(re.sub(r'\s+',' ',label).strip().lower().rstrip(':'),None)==key:
    verified.pop(label,None)
 for key in ('first_name','last_name','email','phone'):
  expected=str(approved.get(key,'')).strip()
  matches=observed.get(key,[])
  label=matches[0][0] if matches else {'first_name':'English First Name','last_name':'English Last Name','email':'Email','phone':'Phone Number'}[key]
  if not matches:
   notes[label]='needs review — identity field not located for final verification'
  elif len(matches)!=1:
   notes[label]='needs review — ambiguous duplicate identity controls'
  elif not expected:
   notes[label]='needs input — approved candidate profile value missing'
  elif matches[0][1]!=expected:
   notes[label]='needs review — final value mismatch (approved profile differs from employer form)'
   report('review','Identity mismatch in '+label+'; do not submit.')
  else:
   verified[label]='verified';notes.pop(label,None)
 upload=_upload(page,resume,report)
 missing=_snapshot_missing(page,resume)
 results=[{'field':k,'result':v} for k,v in verified.items()]
 results.append(upload)
 results.extend(dropdown_verified)
 results.extend(dropdown_issues)
 for k,v in notes.items():
  if k not in verified:results.append({'field':k,'result':v})
 existing={r['field'] for r in results}
 for m in missing:
  if m['field'] not in existing:results.append(m)
 # Audit explicit visible unanswered questions even when sites omit required attribute.
 for frame in page.frames:
  try:
   checks=frame.locator('input[type="checkbox"],input[type="radio"]')
   for i in range(min(checks.count(),80)):
    el=checks.nth(i)
    if not el.is_visible() or not el.is_enabled():continue
    label=_label(el)
    if not label or label in existing:continue
    required=el.get_attribute('required') is not None or el.get_attribute('aria-required')=='true'
    if required:results.append({'field':label,'result':'needs input — declaration requires candidate approval'});existing.add(label)
  except Exception:pass
 if not verified and not dropdown_verified:results.append({'field':'Application fields','result':'needs review — no text fields verified'})
 report('running',f'Form audit complete: {len(verified)} text fields and {len(dropdown_verified)} dropdowns verified; {len(missing)} required controls unresolved. No submission.')
 return results
