"""Safe, bounded selection of candidate-approved country/dial code in native and custom widgets."""
import re


def label_of(el):
 return el.evaluate(r'''e=>{let a=[...(e.labels||[])].map(x=>x.innerText).join(' ');let b=e.getAttribute('aria-labelledby');if(!a&&b)a=b.split(/\s+/).map(i=>document.getElementById(i)?.innerText||'').join(' ');if(!a)a=e.getAttribute('aria-label')||e.closest('label')?.innerText||'';if(!a){let p=e.closest('[class*="field"],[class*="question"],[role="group"]')||e.parentElement;a=p?.querySelector('label')?.innerText||p?.querySelector('legend')?.innerText||p?.previousElementSibling?.innerText||''}return a.replace(/\s+/g,' ').trim().slice(0,150)}''')


def approved_targets(profile):
 loc=str(profile.get('location',''))
 country=str(profile.get('country','')).strip()
 if not country and re.search(r'\bIndia\b',loc,re.I):country='India'
 code=str(profile.get('phone_country_code','')).strip()
 # Dial code may be inferred from an explicitly approved India country only.
 if not code and country.casefold()=='india':code='+91'
 return country,code


def _matches(text,target,kind):
 t=re.sub(r'\s+',' ',text).strip().casefold()
 if kind=='country':return t==target.casefold()
 if kind=='code':return bool(re.search(r'(?<!\d)\+?91(?!\d)',t)) and ('india' in t or t in ('+91','91'))
 return False


def _select_native(el,target,kind):
 options=el.locator('option');matches=[]
 for i in range(min(options.count(),400)):
  opt=options.nth(i)
  if _matches(opt.inner_text(timeout=800),target,kind):matches.append(opt.get_attribute('value'))
 if len(matches)!=1:return False
 el.select_option(value=matches[0],timeout=3500)
 return el.input_value()==matches[0]


def _visible_options(frame):
 """Collect candidates from common portal overlays, including portalled menus."""
 selectors='[role="option"], [role="listbox"] li, [role="menuitem"], [data-radix-collection-item], [cmdk-item], [aria-selected], .select__option, .react-select__option, .dropdown-item, .ant-select-item-option, .MuiAutocomplete-option, li'
 loc=frame.locator(selectors)
 found=[]
 for i in range(min(loc.count(),650)):
  item=loc.nth(i)
  try:
   if not item.is_visible():continue
   txt=item.inner_text(timeout=350).strip()
   if txt and len(txt)<180:found.append((item,txt))
  except Exception:continue
 return found


def _option_candidates(frame):
 "Visible options inside floating scrollable menus."
 selectors=('[role="option"], [role="listbox"] li, [role="menuitem"], '
            '[data-radix-collection-item], [cmdk-item], [aria-selected], '
            '.select__option, .react-select__option, .dropdown-item, '
            '.ant-select-item-option, .MuiAutocomplete-option, '
            '[class*="dropdown"] li, [class*="option"], li')
 loc=frame.locator(selectors)
 out=[]
 for i in range(min(loc.count(),850)):
  el=loc.nth(i)
  try:
   if not el.is_visible():continue
   text=' '.join(el.inner_text(timeout=220).split())
   if 0<len(text)<125:out.append((el,text))
  except Exception:pass
 return out


def _scroll_menus(frame, direction=1):
 "Scroll open menu containers, not the page."
 return frame.evaluate(r"""direction=>{
 const selectors=['[role="listbox"]','[role="menu"]','[class*="dropdown"]','[class*="menu"]','[class*="option"]','[class*="select"]'];
 const nodes=new Set();
 for(const selector of selectors)for(const el of document.querySelectorAll(selector)){
  let p=el;for(let i=0;p&&i<5;i++,p=p.parentElement)nodes.add(p);
 }
 const scrollables=[...nodes].filter(e=>{
  const r=e.getBoundingClientRect(),css=getComputedStyle(e);
  return r.width>90&&r.height>55&&r.top<innerHeight&&r.bottom>0&&
   e.scrollHeight>e.clientHeight+25&&/(auto|scroll)/.test(css.overflowY);
 }).sort((a,b)=>a.getBoundingClientRect().height-b.getBoundingClientRect().height);
 if(!scrollables.length)return {found:false};
 const el=scrollables[0],before=el.scrollTop;
 el.scrollTop=Math.max(0,Math.min(el.scrollHeight-el.clientHeight,before+direction*Math.max(100,el.clientHeight*.75)));
 el.dispatchEvent(new Event('scroll',{bubbles:true}));
 return {found:true,moved:Math.abs(el.scrollTop-before)>2,top:el.scrollTop,height:el.scrollHeight};
 }""",direction)


def _read_selection(el):
 return el.evaluate(r"""e=>{
  const text=[e.value||'',e.innerText||'',e.getAttribute('aria-valuetext')||'',e.getAttribute('title')||'',e.getAttribute('aria-label')||''];
  const p=e.parentElement;if(p)text.push(p.innerText||'');
  return text.join(' ').slice(0,260);
 }""")


def _selected_text(el):
 """Read only the trigger, not its parent (which can include every menu option)."""
 return el.evaluate(r"""e=>{
  const attrs=[e.value,e.getAttribute('aria-valuetext'),e.getAttribute('data-value')];
  const displayed=e.querySelector('[class*="singleValue"], [class*="selected"], [data-value]');
  return [...attrs,e.innerText,displayed?.innerText].filter(Boolean).join(' ').trim().slice(0,180);
 }""")


def _persisted(frame,el,target,kind):
 """Verify after blur, DOM updates, and a second independent read."""
 try:
  frame.wait_for_timeout(650)
  first=_selected_text(el)
  if not _matches(first,target,kind):return False
  el.press('Tab',timeout=1200)
  frame.wait_for_timeout(750)
  return _matches(_selected_text(el),target,kind)
 except Exception:return False


def _menu_options(frame):
 """Prefer semantic options; avoid matching arbitrary list items elsewhere on page."""
 selectors=('[role="listbox"] [role="option"], [role="option"], '
            '[role="listbox"] li, [role="menu"] [role="menuitem"], '
            '.select__option, .react-select__option, .ant-select-item-option, '
            '.MuiAutocomplete-option, [data-radix-collection-item]')
 loc=frame.locator(selectors)
 found=[]
 for i in range(min(loc.count(),500)):
  item=loc.nth(i)
  try:
   if item.is_visible():
    txt=' '.join(item.inner_text(timeout=400).split())
    if 0<len(txt)<140:found.append((item,txt))
  except Exception:pass
 return found


def _select_custom(frame,el,target,kind,report=None):
 """Real UI click, bounded menu search, and delayed state verification."""
 def log(msg):
  if report:report('running','Dropdown '+kind+': '+msg)
 try:
  el.scroll_into_view_if_needed(timeout=2500)
  el.click(timeout=3500)
  frame.wait_for_timeout(450)
 except Exception as exc:
  log('trigger click failed: '+type(exc).__name__)
  return False
 visited=set()
 for step in range(32):
  options=_menu_options(frame)
  matches=[(item,txt) for item,txt in options if _matches(txt,target,kind)]
  if matches:
   # Prefer a real option over a containing list item.
   matches.sort(key=lambda pair: 0 if pair[0].get_attribute('role')=='option' else 1)
   for item,txt in matches[:3]:
    try:
     item.scroll_into_view_if_needed(timeout=1600)
     item.click(timeout=2500)
     if _persisted(frame,el,target,kind):
      log('selection persisted after blur')
      return True
     log('option clicked but selection did not persist')
     el.click(timeout=2000)
     frame.wait_for_timeout(250)
    except Exception as exc:log('option interaction failed: '+type(exc).__name__)
   break
  if step==0:
   log('opened menu; visible option count '+str(len(options)))
   # Search only an input inside an opened listbox, never the page's unrelated search.
   search=frame.locator('[role="listbox"] input, [role="menu"] input')
   active=[search.nth(i) for i in range(min(search.count(),12)) if search.nth(i).is_visible()]
   if len(active)==1:
    try:
     active[0].fill('India' if kind=='code' else target,timeout=1600)
     frame.wait_for_timeout(350)
     continue
    except Exception:pass
  state=_scroll_menus(frame)
  if not state.get('found') or not state.get('moved'):break
  fp=(round(state.get('top',0)),state.get('height',0))
  if fp in visited:break
  visited.add(fp)
  frame.wait_for_timeout(170)
 log('matching option not selected or not retained')
 try:el.press('Escape',timeout=700)
 except Exception:pass
 return False

def complete_dropdowns(page,profile,report):
 country,code=approved_targets(profile);done=[];issues=[]
 for frame in page.frames:
  # Includes custom buttons with aria-haspopup=listbox, not just ARIA comboboxes.
  controls=frame.locator('select,[role="combobox"],button[aria-haspopup="listbox"],[aria-haspopup="listbox"],button[aria-expanded], [role="button"][aria-expanded]')
  try:count=min(controls.count(),120)
  except Exception:continue
  for i in range(count):
   el=controls.nth(i);label='Dropdown'
   try:
    if not el.is_visible() or not el.is_enabled():continue
    label=label_of(el)
    # Fallback: nearby visible label, common in custom ATS widgets.
    if not label:
     label=el.evaluate(r'''e=>{let p=e.parentElement;return (p?.previousElementSibling?.innerText||p?.parentElement?.querySelector('label')?.innerText||'').trim().slice(0,100)}''')
    if re.search(r'country\s*code|dial(?:ing)?\s*code|phone\s*prefix',label,re.I):kind='code';target=code
    elif re.fullmatch(r'country\s*(?:of residence)?\s*[:\s*]*',label.strip(),re.I):kind='country';target=country
    else:continue
    if not target:issues.append({'field':label,'result':'needs input — approve country or dial code in candidate profile'});continue
    tag=el.evaluate('e=>e.tagName')
    current=el.input_value() if tag=='SELECT' else _selected_text(el)
    if _matches(current,target,kind):done.append({'field':label,'result':'verified — approved dropdown selection'});continue
    ok=_select_native(el,target,kind) if tag=='SELECT' else _select_custom(frame,el,target,kind,report)
    if ok:done.append({'field':label,'result':'verified — approved dropdown selection persisted'});report('running','Verified dropdown: '+label)
    else:issues.append({'field':label,'result':'needs review — could not select and verify approved option'})
   except Exception as exc:issues.append({'field':label,'result':'needs review — interaction failed: '+str(exc)[:90]})
 return done,issues
