"""Deterministic query interpretation and ranking; input and records are data only."""
import json
import math
import re
from collections import Counter

STOP = set('a an the and or of to in on for from with at is are was were be been who what when where which how did does do can you me tell about please find show list give all washington dc united states us embassy embassies diplomat diplomats ambassador ambassadors'.split())
INTENTS = {
    'properties': r'\b(address|located|location|architect|architecture|built|constructed|building|floor|square|sqft|acre|residence|chancery|property|properties|assessed|assessment|worth|size|big)\b',
    'countries': r'\b(relations|recognition|recognized|recognised|bilateral|relationship|diplomacy)\b',
    'appointments': r'\b(ambassador|ambassadors|envoy|envoys|minister|ministers|charge|chief|chiefs|served|serving|appointed|appointments|roster|tenure)\b',
}
FIELDS = ('name','title','country','countrySlug','countrySlugs','affiliations','documentedRoles','slug','legalName','aliases','topics','expandedParagraphs','summary','description','achievements','collectionNarrative','address','facts','period','representedEntity')


def canonical_text(record):
    """Search article content and historical dates, never source capture dates."""
    fields = ('displayName', 'aliases', 'countrySlugs', 'topics', 'address', 'dateText',
              'validTime', 'bilateralStates', 'representedGovernment')
    text = [json.dumps(record.get(field, ''), ensure_ascii=False) for field in fields]
    for alias in record.get('addressAliases', []):
        text.extend(value for value in (alias.get('address'), alias.get('note')) if value)
        text.append(json.dumps(alias.get('validTime'), ensure_ascii=False))
    for occupancy in record.get('occupancy', []):
        text.extend(value for value in (occupancy.get('label'), occupancy.get('use')) if value)
        text.append(json.dumps(occupancy.get('validTime'), ensure_ascii=False))
    for event in record.get('placeChronology', []):
        text.extend(value for value in (event.get('kind'), event.get('label'), event.get('description')) if value)
        text.append(json.dumps(event.get('validTime'), ensure_ascii=False))
    text.extend(' '.join([event['kind'], event.get('label', ''), event['date']['value']])
                for event in record.get('chronology', []))
    for event in record.get('bilateralChronology', []):
        text.extend(value for value in (event.get('type'), event.get('label'), event.get('description')) if value)
        text.append(json.dumps(event.get('validTime'), ensure_ascii=False))
    for section in record.get('sections', []):
        text.append(section.get('heading', ''))
        text.extend(p.get('text', '') for p in section.get('paragraphs', []))
    text.extend(member['rationale'] for member in record.get('curatedReading', []))
    for note in record.get('notes', []):
        note_text = note.get('text', '') if isinstance(note, dict) else note
        if isinstance(note_text, str) and note_text.strip():
            text.append(note_text)
    return ' '.join(text)


def canonical_dates(client, record, year, rows, linked=False):
    """Date evidence keeps attestations separate from continuous service."""
    from chanceries_agent import year_match
    if year is None:
        return []
    if record['recordType'] in {'person', 'appointment'}:
        ids = record.get('appointmentIds', []) if record['recordType'] == 'person' else [record['id']]
        result = []
        for identifier in ids:
            row = rows.get(identifier)
            basis = year_match(row, year, client.catalogue['asOf']) if row else ''
            if basis:
                result.append({'id': identifier, 'basis': basis})
        return result
    if record['recordType'] == 'topic' and record.get('curatedReading'):
        # Editorial membership does not date a topic. Match only years in the
        # authored narrative, using the same extraction as browser search.
        paragraphs = ' '.join(p.get('text', '') for section in record.get('sections', [])
                              for p in section.get('paragraphs', []))
        mentioned = set(re.findall(r'(?<!\d)(?:1[7-9]\d{2}|20\d{2})(?!\d)', paragraphs))
        if str(year) in mentioned:
            return [{'id': record['id'],
                     'basis': str(year) + ' is mentioned in the article narrative; no dated event or service interval is asserted',
                     'scope': 'narrative mention'}]
        return []
    scopes = [('record', record.get('validTime'), {})]
    scopes += [('bilateral state', state.get('validTime'), {}) for state in record.get('bilateralStates', [])]
    scopes += [('bilateral chronology', event.get('validTime'), {}) for event in record.get('bilateralChronology', [])]
    scopes += [('occupancy', use.get('validTime'),
                {'occupancyId': use.get('id'), 'occupantId': use.get('occupantId'),
                 'missionReference': use.get('missionReference'), 'use': use.get('use')})
               for use in record.get('occupancy', [])]
    scopes += [('place chronology', event.get('validTime'),
                {'chronologyId': event.get('id'), 'chronologyKind': event.get('kind')})
               for event in record.get('placeChronology', [])]
    result = []
    for scope, interval, context in scopes:
        if not isinstance(interval, dict):
            continue
        attested_dates = set()
        for dated in interval.get('attestations', []):
            dated = dated.get('date', dated) if isinstance(dated, dict) else {'value': dated}
            if str(dated.get('value', ''))[:4] == str(year):
                attested_dates.add(dated['value'])
                result.append({'id': record['id'], 'basis': 'Dated evidence; continuity unconfirmed',
                               'scope': scope, 'date': dated['value'], **context})
        if interval.get('continuity') != 'interval':
            # A known endpoint can be discovered without asserting continuous
            # use before/after it. Do not fill intervening or open-ended years.
            if interval.get('continuity') == 'unestablished':
                for endpoint, label in (('start', 'Start'), ('end', 'End')):
                    dated = interval.get(endpoint)
                    if (dated and str(dated.get('value', ''))[:4] == str(year)
                            and dated['value'] not in attested_dates):
                        result.append({'id': record['id'], 'basis': label + ' recorded; continuity unconfirmed',
                                       'scope': scope, 'date': dated['value'], **context})
            continue
        start, end = interval.get('start'), interval.get('end')
        sy = int(start['value'][:4]) if start else None
        ey = int(end['value'][:4]) if end else None
        if sy is not None and ey is not None and sy <= year <= ey:
            imprecise = (start.get('precision') != 'day' and sy == year) or (end.get('precision') != 'day' and ey == year)
            result.append({'id': record['id'], 'basis': 'Possible overlap; dates imprecise' if imprecise else 'Documented interval overlaps this year', 'scope': scope, **context})
        elif sy == year and ey is None:
            result.append({'id': record['id'], 'basis': 'Start recorded; endpoint unknown', 'scope': scope, **context})
        elif ey == year and sy is None:
            result.append({'id': record['id'], 'basis': 'End recorded; beginning unknown', 'scope': scope, **context})
    if record['recordType'] == 'topic' and not linked:
        # A collection inherits discoverable dates from its evidenced membership,
        # not a new claim that the topic itself existed continuously in that year.
        targets = {a.get('targetId') for a in client.canonical.get('assertions', [])
                   if a.get('subjectId') == record['id'] and a.get('predicate') in {'includes', 'concerns'}}
        for target in sorted(t for t in targets if t in client.canonical_by_id):
            for evidence in canonical_dates(client, client.canonical_by_id[target], year, rows, linked=True):
                result.append({**evidence, 'linkedRecordId': target, 'scope': 'linked record'})
    return result


def ranked_search(client, query='', kind='all', country=None, year=None, offset=0, limit=20):
    from chanceries_agent import KINDS, CANONICAL_KINDS, RECORD_KINDS, norm, year_match, canonical_url
    client.load()
    if kind not in (*RECORD_KINDS, 'all') or not isinstance(query,str) or len(query)>500:
        raise ValueError('Invalid search parameters')
    if type(offset) is not int or offset<0 or type(limit) is not int or not 1<=limit<=100:
        raise ValueError('Use offset >= 0 and limit between 1 and 100')
    q=norm(query)
    requested_kind = kind
    if re.search(r'\b(?:u\.?s\.?|united states|american)\s+(?:ambassadors?|envoys?|embassy|embassies)\s+(?:to|in)\b',q):
        return client.envelope({'queryInterpretation':{'unsupportedIntent':'This dataset covers foreign representation in Washington, not U.S. ambassadors or embassies abroad.'},'total':0,'offset':offset,'limit':limit,'nextOffset':None,'results':[]})
    if re.search(r'\b(?:book|schedule|apply for|check status).*\b(?:visa|passport)|\b(?:opening hours|office hours|visa appointment)\b', q):
        return client.envelope({'queryInterpretation': {'unsupportedIntent': 'Visa/passport booking and live office hours are outside this historical/property research dataset. Consult the official mission.'}, 'total': 0, 'offset':offset, 'limit':limit, 'nextOffset':None, 'results':[]})
    slug=client.country(country)
    inferred={}
    # Longest recorded country alias wins; word boundaries prevent US in words.
    if not slug:
        candidates=[]
        for c in client.data['countries']:
            for alias in [c['id'],c.get('country'),c.get('legalName'),*c.get('aliases',[])]:
                alias=norm(alias)
                if len(alias)>1 and re.search(r'(?<!\w)'+re.escape(alias)+r'(?!\w)',q):
                    candidates.append((len(alias),c['id'],alias))
        if candidates:
            _,slug,alias=sorted(candidates,reverse=True)[0]
            q=re.sub(r'(?<!\w)'+re.escape(alias)+r'(?!\w)',' ',q)
            inferred['country']=slug
    if kind=='all' and re.search(r'\bwhere (?:is|are)\b',q):
        kind='properties';inferred['kind']=kind
    if kind=='all':
        for intent,pattern in INTENTS.items():
            if re.search(pattern,q):kind=intent;inferred['kind']=kind;break
    if year is None and kind in ('people','appointments', *CANONICAL_KINDS):
        match=re.search(r'\b(?:in|during|year)\s+((?:17|18|19|20)\d{2})\b',q)
        if match:year=int(match[1]);q=q.replace(match[1],' ');inferred['year']=year
    if year is not None and (type(year) is not int or not 1700<=year<=int(client.catalogue['asOf'][:4])):
        raise ValueError('Year must be within the documented catalogue period')
    if year is not None and kind not in ('people','appointments', *CANONICAL_KINDS):
        raise ValueError('Use a people/appointments collection or a canonical record type for year filtering')
    current=bool(re.search(r'\b(current|currently|now|today)\b|\bwho is\b.*\b(?:ambassador|envoy|chief of mission)\b',q)) and kind in ('people','appointments','person','appointment')
    if current:inferred['status']='Current as recorded; check verifiedOn'
    q=re.sub(r'\b(current|currently|now|today|who|what|when|where|which|how|year|during)\b',' ',q)
    terms=[x for x in re.findall(r'[\w]+',q) if x not in STOP and len(x)>1]
    # Wording that names a field must not exclude matching records containing its label.
    synonyms={'large':'area','big':'area','size':'area','built':'constructed','worth':'value','sqft':'area','footage':'area','architects':'architect','started':'established'}
    terms=list(dict.fromkeys(synonyms.get(x,x) for x in terms))
    names={c['country']:c['id'] for c in client.data['countries']}
    evidence={}
    for r in client.data['appointments']:
        basis=year_match(r,year,client.catalogue['asOf']) if year is not None else ''
        if basis:evidence[r['id']]=basis
    current_ids={r.get('biographyId') or r['personId'] for r in client.data['appointments'] if r.get('status')=='Current'}
    rows = {r['id']: r for r in client.data['appointments']}
    corpus=[]
    canonical_events = {r['id']: r for r in client.canonical.get('records', [])
                        if r['recordType'] == 'event' and r.get('article')}
    canonical_places = {canonical_url('place', r): r for r in client.canonical.get('records', [])
                        if r['recordType'] == 'place'}
    for group in KINDS if kind=='all' else [kind] if kind in KINDS else []:
        for r in client.data[group]:
            # Canonical event articles supersede their compatibility card
            # projections in ordinary search, retaining every old key as alias.
            if group == 'events' and kind == 'all' and r.get('canonicalRecordId') in canonical_events:
                continue
            c=r.get('countrySlug') or r.get('slug') or r.get('country');c=names.get(c,c)
            linked = list(dict.fromkeys(value for value in [c, *r.get('countrySlugs', []),
                *[a.get('countrySlug') for a in r.get('affiliations', [])]] if value))
            if slug and slug not in linked:continue
            if group == 'people' and slug:
                c = slug
            if current and ((group=='appointments' and r.get('status')!='Current') or (group=='people' and r['id'] not in current_ids)):continue
            ids=r.get('appointmentIds',[]) if group=='people' else [r['id']]
            service=[{'id':i,'basis':evidence[i]} for i in ids if i in evidence]
            if year is not None and not service:continue
            text=norm(' '.join(json.dumps(r.get(f,''),ensure_ascii=False) for f in FIELDS))
            if group == 'properties':
                place = canonical_places.get(canonical_url(group, r))
                if place:
                    text += ' ' + norm(canonical_text(place))
            title=norm(r.get('name') or r.get('title') or r.get('legalName') or r.get('country'))
            corpus.append((group,r,c,service,text,title,[]))
    canonical_candidates = []
    if kind in CANONICAL_KINDS:
        canonical_candidates = [r for r in client.canonical.get('records', []) if r['recordType'] == kind]
    elif requested_kind == 'all' and not current and year is None:
        # Existing people, country, property, and event URLs remain the all-search
        # entries. Only additional readable articles join ordinary discovery.
        legacy_urls = {canonical_url(group, r) for group in KINDS for r in client.data[group]}
        canonical_candidates = [r for r in client.canonical.get('records', [])
            if r.get('article') and r['recordType'] not in {'person', 'appointment'}
            and (r['recordType'] == 'event' or canonical_url(r['recordType'], r) not in legacy_urls)]
    for r in canonical_candidates:
        group = r['recordType']
        slugs = r.get('countrySlugs', [])
        if slug and slug not in slugs:
            continue
        if current:
            if group == 'person' and r['id'] not in current_ids:
                continue
            if group == 'appointment' and rows.get(r['id'], {}).get('status') != 'Current':
                continue
        dated = canonical_dates(client, r, year, rows)
        if year is not None and not dated:
            continue
        service = dated if group in {'person', 'appointment'} else []
        date_evidence = dated if group not in {'person', 'appointment'} else []
        corpus.append((group, r, slug or (slugs[0] if slugs else None), service,
                       norm(canonical_text(r)), norm(r['displayName']), date_evidence))
    freq={t:sum(t in x[4] for x in corpus) for t in terms}
    matches=[]
    for group,r,c,service,text,title,date_evidence in corpus:
        hits=[t for t in terms if t in text]
        if terms and not hits:continue
        score=sum((1+math.log(1+len(corpus)/(1+freq[t])))*(3 if t in title else 1) for t in hits)
        if terms:score*=len(hits)/len(terms)
        if norm(query).strip() and norm(query).strip() in title:score+=20
        if group == 'countries' and norm(query).strip() in {norm(x) for x in [r['id'], r.get('country'), r.get('legalName'), *r.get('aliases', [])]}:
            score += 40
        if current:score+=5
        narrative = canonical_places.get(canonical_url(group, r), r) if group == 'properties' else r
        snippet = (' '.join(p.get('text', '') for section in narrative.get('sections', []) for p in section.get('paragraphs', []))
                   or ' '.join(r.get('expandedParagraphs',[])) or r.get('summary') or r.get('achievements')
                   or r.get('description') or ' '.join(r.get('collectionNarrative',[])))[:350]
        matches.append((score,{'kind':group,'id':r['id'],'name':r.get('displayName') or r.get('name') or r.get('title') or r.get('legalName') or r.get('country'),
          'country':c,'url':canonical_url('appointment',client.canonical_by_id[r['id']]) if group=='appointments' and r['id'] in client.canonical_by_id else canonical_url(group,r),'reviewedOn':r.get('reviewedOn'),'verifiedOn':r.get('verifiedOn'),
          'researchStatus':r.get('researchStatus'),'status':r.get('status'),
          'snippet':snippet,'serviceEvidence':service,
          **({'countrySlugs': r['countrySlugs']} if group == 'people' and r.get('countrySlugs') else {}),
          **({'documentedRoles': r['documentedRoles']} if group == 'people' and r.get('documentedRoles') else {}),
          **({'recordType': group, 'dateEvidence': date_evidence, 'countrySlugs': r.get('countrySlugs', [])} if group in CANONICAL_KINDS else {})}))
    matches.sort(key=lambda x:(-x[0],norm(x[1]['name']),x[1]['kind'],x[1]['id']))
    return client.envelope({'queryInterpretation':inferred,'total':len(matches),'offset':offset,'limit':limit,
      'nextOffset':offset+limit if offset+limit<len(matches) else None,'results':[r for _,r in matches[offset:offset+limit]]})





