"""Rebuild Schedule-1 contracts from reviewed blanks; additive seed, no DB writes."""
import hashlib
import json
from pathlib import Path
import xml.etree.ElementTree as E
import pikepdf

BASE = Path(__file__).resolve().parents[2]
PREFIX = 'master_entry_level.imm5257sch1.'
GROUPS = {
    'military': ('MilitaryServiceInfo', 'ServedInMilitary', 'MilitaryServiceDetails', 'MilitaryServiceDetail', 'Military service', 'Service militaire'),
    'ill_treatment': ('WarHumanityCrimesInfo', 'HaveWitnessedParticipated', 'WarHumanityCrimesDetails', 'WarHumanityCrimesDetail', 'Ill-treatment', 'Mauvais traitements'),
    'organizations': ('MembershipAssociationInfo', 'BeenMemberAssociated', 'MembershipAssociationDetails', 'MembershipAssociationDetail', 'Organizations', 'Organisations'),
    'government': ('GovernmentPositionsInfo', 'HeldGovernmentPositions', 'GovernmentPositionsDetails', 'GovernmentPositionsDetail', 'Government positions', 'Fonctions gouvernementales'),
    'travel': ('PreviousTravelInfo', 'TraveledOtherCountry', 'PreviousTravelDetails', 'PreviousTravelDetail', 'Previous travel', 'Voyages antérieurs'),
}
LEAVES = {
    'From.Year': ('from_date', 'From date', 'Date de début', 'year'), 'From.Month': ('from_date', 'From date', 'Date de début', 'month'),
    'To.Year': ('to_date', 'To / confirmed reporting-through date', 'Date de fin / date de déclaration confirmée', 'year'), 'To.Month': ('to_date', 'To / confirmed reporting-through date', 'Date de fin / date de déclaration confirmée', 'month'),
    'Location': ('location', 'City / location', 'Ville / lieu', 'text'), 'Province': ('province', 'Province / state', 'Province / État', 'text'),
    'CountryCode': ('country', 'Country', 'Pays', 'country'), 'Details': ('details', 'Details', 'Précisions', 'text'),
    'NameOfOrganization': ('organization', 'Organization name', 'Nom de l’organisation', 'text'),
    'ActivitiesPositionHeld': ('activities', 'Activities / position held', 'Activités / poste occupé', 'text'),
    'LevelOfJurisdiction': ('jurisdiction', 'Jurisdiction level', 'Niveau de compétence', 'text'),
    'DepartmentBranch': ('department', 'Department / branch', 'Ministère / direction', 'text'),
    'PurposeOfTravel': ('purpose', 'Purpose of travel', 'Motif du voyage', 'text'),
}
IDENTITY = {
    'PrincipalApplicant': ('role', 'Applicant role', 'Rôle du demandeur', 'role'),
    'FamilyName': ('family_name', 'Family name (passport)', 'Nom de famille (passeport)', 'upper'),
    'GivenName': ('given_names', 'Given names (passport)', 'Prénoms (passeport)', 'upper'),
    'ApplicantBirthDate.Year': ('dob', 'Date of birth', 'Date de naissance', 'year'),
    'ApplicantBirthDate.Month': ('dob', 'Date of birth', 'Date de naissance', 'month'),
    'ApplicantBirthDate.Day': ('dob', 'Date of birth', 'Date de naissance', 'day'),
    'UCI': ('uci', 'UCI', 'IUC', 'uci'),
}

def local(tag):
    return tag.rsplit('}', 1)[-1]

def dump(path, obj):
    path.write_text(json.dumps(obj, ensure_ascii=False, indent=4 if path.name in ['master_entry_level_questionnaire.json','template_versions.json'] else 2)+'\n', encoding='utf-8')

def build():
    versions_path = BASE/'resources/imm/template_versions.json'
    versions = json.loads(versions_path.read_text(encoding='utf-8'))
    versions['IMM5257SCH1'] = {}
    maps = {}
    for lang, suffix in [('en', 'e'), ('fr', 'f')]:
        blank = BASE/f'storage/app/imm/imm5257_1{suffix}.pdf'
        with pikepdf.open(blank) as pdf:
            a = pdf.Root.AcroForm.XFA
            packets = {str(a[i]): E.fromstring(a[i+1].read_bytes()) for i in range(0,len(a),2) if str(a[i]) in ['template','datasets']}
        fields, rows = {}, []
        tables = {}
        def walk(node, base='Schedule1', som=''):
            name = node.get('name')
            som = som + ('.'+name if name else '')
            binding = next((c for c in node if local(c.tag)=='bind' and c.get('ref')), None)
            target = None
            if binding is not None:
                target = base+'.'+binding.get('ref').removeprefix('$.')
                if local(node.tag)=='subform':
                    base = target.replace('[*]', '[{row}]')
                    occur = next(c for c in node if local(c.tag)=='occur')
                    tables[base] = {'initial':int(occur.get('initial')), 'min':int(occur.get('min')), 'max':int(occur.get('max')), 'template_path':som.lstrip('.')}
                    group=next(k for k,g in GROUPS.items() if base.startswith('Schedule1.'+g[0]+'.'))
                    tables[base]['active_when']={'path':'Schedule1.'+GROUPS[group][0]+'.'+GROUPS[group][1],'equals':'true'}
            if target and local(node.tag) in ['field','exclGroup']:
                leaf = target.removeprefix('Schedule1.')
                group = next((k for k,g in GROUPS.items() if leaf.startswith(g[0]+'.')), None)
                if leaf in IDENTITY:
                    key,en,fr,kind = IDENTITY[leaf]
                    source=PREFIX+key
                elif group:
                    g=GROUPS[group]
                    if leaf == g[0]+'.'+g[1]:
                        key=group; en=g[4]+' — Declaration';fr=g[5]+' — Déclaration';kind='yes';source=PREFIX+key
                    else:
                        tail=leaf.split('[{row}].')[-1]
                        key,en,fr,kind=LEAVES[tail]
                        en=g[4]+' {number} — '+en;fr=g[5]+' {number} — '+fr
                        source=PREFIX+group+'_rows.{row}.'+key
                else:
                    return
                meta={'label':fr if lang=='fr' else en, 'template_path':som.lstrip('.')}
                meta['template_required']=node.get('mandatory')=='error' or any(local(c.tag)=='validate' and c.get('nullTest')=='error' for c in node)
                meta['optional']=leaf=='GivenName' or (group is not None and target.endswith('.Province'))
                limits=[int(c.get('maxChars')) for c in node.iter() if c.get('maxChars') and int(c.get('maxChars'))>0]
                if limits:meta['max_chars']=min(limits)
                if kind in ['yes','role']:
                    meta['allowed_values']=list(dict.fromkeys(t.text for f in node if local(f.tag)=='field' for c in f if local(c.tag)=='items' for t in c if t.text))
                    assert set(meta['allowed_values'])=={'true','false'}
                if kind=='country':
                    choices={c.get('lic'):''.join(c.itertext()).strip() for c in packets['datasets'].iter() if local(c.tag)=='CountryOfBirth' and c.get('lic')}
                    assert choices
                    meta['choices']=choices
                fields[target]=meta
                rows.append(dict(xfa=target, source=source, kind=kind, group=group, label=en,label_fr=fr))
            for child in node:walk(child,base,som)
        walk(packets['template'])
        assert len(rows)==50, len(rows)
        prompts=[' '.join(' '.join(c.itertext()).split()) for n in packets['template'].iter() if local(n.tag)=='draw' and n.get('name')=='military' for c in n if local(c.tag)=='value']
        assert len(prompts)==5
        internal_version=packets['datasets'].find('{http://www.xfa.org/schema/xfa-data/1.0/}data/Schedule1/FormVersion').text
        contract={'form':'IMM5257SCH1','language':lang,'official_revision':'September 2023','internal_form_version':internal_version,'sha256':hashlib.sha256(blank.read_bytes()).hexdigest(),'fields':fields,'tables':tables,'declaration_prompts':dict(zip(GROUPS,prompts)),
                  'date_rules':{'minimum_year':1900,'template_maximum_year':'current year + 1','template_maximum_month':'current month next year','autofill_completed_history':'not future','precision':'month','ordered_range':True},
                  'manual':{'Schedule1.Signatures': 'Examiner et signer dans Adobe Reader' if lang=='fr' else 'Review and sign in Adobe Reader','Schedule1.FormValidated':'Valider dans Adobe Reader' if lang=='fr' else 'Validate in Adobe Reader','Schedule1.ApplicationValidationDate':'Date de validation — manuelle' if lang=='fr' else 'Validation date — manual'}}
        dump(BASE/f'resources/imm/imm5257sch1-contract.{lang}.json',contract)
        mapping={'form':'IMM5257SCH1','pdf':blank.name,'fields':rows}
        dump(BASE/f'resources/imm/maps/imm5257sch1{".fr" if lang=="fr" else ""}.json',mapping)
        maps[lang]=mapping
        versions['IMM5257SCH1'][lang]={'pdf':blank.name,'sha256':contract['sha256']}
    dump(versions_path,versions)
    seed(maps['en']['fields'],maps['fr']['fields'])

def seed(rows, french):
    questions=[]
    def rule(parent):
        return {'all':[{'field':PREFIX+parent,'op':'in','value':['Yes','yes']}]}
    def add(key,en,fr,typ='short_text',**kwargs):
        questions.append(dict(source_key=PREFIX+key,label=en,label_fr=fr,question_type=typ,required_default=False,options=None,field_schema=None,visibility_rule=rule('confirmation') if key!='confirmation' else None,display_order=len(questions)+1,**kwargs))
    add('confirmation','Do you need to complete additional background and travel history for this applicant (IMM5257 Schedule 1)?','Devez-vous fournir les antécédents et voyages supplémentaires pour ce demandeur (annexe 1 IMM5257)?','yes_no',help_text='Choose Yes if IMM5257 Schedule 1 is part of this application. If unsure, ask your consultant. Use this applicant’s own information when answering the questions that follow.',help_text_fr='Choisissez Oui si l’annexe 1 IMM5257 fait partie de cette demande. En cas de doute, consultez votre consultant. Répondez aux questions suivantes avec les renseignements propres à ce demandeur.')
    add('role','Applicant role for this Schedule 1','Rôle du demandeur pour cette annexe 1','single_select')
    questions[-1]['options']=[{'value':k,'label':en,'label_fr':fr} for k,en,fr in [('principal','Principal applicant','Demandeur principal'),('spouse','Spouse','Époux'),('common_law','Common-law partner','Conjoint de fait'),('adult_dependent','Dependent child aged 18 or older','Enfant à charge de 18 ans ou plus')]]
    for key,en,fr,typ in [('family_name','Passport family name override (if needed)','Remplacement du nom de famille du passeport (au besoin)','short_text'),('given_names','Passport given names override (if needed)','Remplacement des prénoms du passeport (au besoin)','short_text'),('dob','Confirmed date of birth override (if needed)','Remplacement de la date de naissance confirmée (au besoin)','date'),('has_uci','Has IRCC issued this applicant a UCI?','IRCC a-t-il attribué un IUC à ce demandeur?','yes_no'),('uci','UCI (8 or 10 digits)','IUC (8 ou 10 chiffres)','short_text'),('citizenship','Country of citizenship (travel exclusions)','Pays de citoyenneté (exclusions des voyages)','single_select'),('residence_country','Current country of residence (travel exclusions)','Pays de résidence actuel (exclusions des voyages)','single_select')]:
        add(key,en,fr,typ)
        if key=='uci':questions[-1]['visibility_rule']={'all':rule('confirmation')['all']+rule('has_uci')['all']}
        if key in ['citizenship','residence_country']:questions[-1]['field_schema']={'registry':{'key':'countries','selection':'single'}}
    prompts={
      'military':('Have you ever served in any military, militia, civil defence unit, security organization or police force, including non-obligatory national service, reserve or voluntary units?','Avez-vous déjà fait partie d’une armée, d’une milice, d’une unité de défense civile, d’un organisme de sécurité ou d’une force de police, y compris les unités de réserve ou volontaires?'),
      'ill_treatment':('Have you ever witnessed or participated in the ill-treatment of prisoners or civilians, looting or desecration of religious buildings?','Avez-vous déjà été témoin de mauvais traitements infligés à des prisonniers ou civils, de pillage ou de profanation d’édifices religieux, ou participé à ces actes?'),
      'organizations':('Have you ever been a member of or associated with a political party or other group or organization which has engaged in or advocated violence as a means to achieving a political or religious objective, or which has been associated with criminal activity at any time?','Avez-vous déjà été membre d’un parti politique, d’un groupe ou d’une organisation ayant utilisé ou prôné la violence pour atteindre un objectif politique ou religieux, ou associé à des activités criminelles?'),
      'government':('Have you ever held any government positions (such as civil servant, judge, police officer, employee in a security organization)?','Avez-vous déjà occupé des fonctions gouvernementales (fonctionnaire, juge, policier, employé d’un organisme de sécurité)?'),
      'travel':('During the past five years, or since your 18th birthday if more recent, have you travelled to any country other than your country of citizenship or current country of residence?','Au cours des cinq dernières années, ou depuis votre 18e anniversaire si plus récent, avez-vous voyagé dans un pays autre que votre pays de citoyenneté ou de résidence actuelle?')}
    actual={lang:json.loads((BASE/f'resources/imm/imm5257sch1-contract.{lang}.json').read_text(encoding='utf-8')) for lang in ['en','fr']}
    for key,g in GROUPS.items():
        add(key,g[4]+' — answer the exact declaration below',g[5]+' — répondez à la déclaration exacte ci-dessous','yes_no',help_text=actual['en']['declaration_prompts'][key],help_text_fr=actual['fr']['declaration_prompts'][key])
        columns={}
        for row in rows:
            if row['group']!=key or '{row}' not in row['source']:continue
            column=row['source'].split('.')[-1]
            leaf=row['xfa'].split('[{row}].')[-1]
            _,en,fr,kind=LEAVES[leaf]
            col={'id':column,'label':en,'label_fr':fr,'type':'short_text'}
            if column in ['from_date','to_date']:col.update(placeholder='YYYY-MM (or YYYY-MM-DD)')
            if kind=='country':col.update(type='single_select',options=[],registry={'key':'countries','selection':'single'})
            limits=[meta.get('max_chars') for path,meta in actual['en']['fields'].items() if path==row['xfa'] and meta.get('max_chars')]
            if kind=='text' and limits:col.update(max_length=min(limits),maxLength=min(limits),help_text=f'PDF maximum {min(limits)} characters; do not abbreviate or truncate facts.')
            columns[column]=col
        columns['ongoing']={'id':'ongoing','label':'Ongoing? If Yes, To must be your confirmed reporting-through date.','label_fr':'En cours? Si oui, la date de fin doit être votre date de déclaration confirmée.','type':'yes_no'}
        add(key+'_rows',g[4]+' — full history (all rows)',g[5]+' — historique complet (toutes les lignes)','repeatable_table')
        questions[-1]['field_schema']={'kind':'repeatable_table','columns':list(columns.values())}
        questions[-1]['visibility_rule']={'all':rule('confirmation')['all']+rule(key)['all']}
        questions[-1]['help_text']='Use confirmed month dates. Never guess unknown dates. Ongoing rows require a confirmed reporting-through date; no automatic current date. Travel: use the later of your 18th birthday or five years ago, excluding citizenship/current residence countries.'
        questions[-1]['help_text_fr']='Utilisez des mois confirmés, sans estimation. Une activité en cours exige une date de déclaration confirmée. Voyages : depuis la plus récente des dates (18e anniversaire ou il y a cinq ans), hors pays de citoyenneté/résidence actuelle.'
    path=BASE/'database/data/master_entry_level_questionnaire.json'
    catalog=json.loads(path.read_text(encoding='utf-8'))
    source=PREFIX.rstrip('.')
    existing=next((s for s in catalog['sections'] if s['source_key']==source),None)
    section={'source_key':source,'slug':'master-entry-level-imm5257sch1','title':'Additional Background and Travel History','title_fr':'Renseignements supplémentaires sur vos antécédents et voyages','description':'Answer for the person named in this application. We ask about military service, involvement in ill-treatment, organizations, government positions and travel. These answers help complete IMM5257 Schedule 1. Ask your consultant if you are unsure whether this form is required.','description_fr':'Répondez pour la personne nommée dans cette demande. Les questions portent sur le service militaire, la participation à des mauvais traitements, les organisations, les fonctions gouvernementales et les voyages. Ces réponses servent à remplir l’annexe 1 IMM5257. En cas de doute sur la nécessité de ce formulaire, consultez votre consultant.','display_order':existing['display_order'] if existing else max(s['display_order'] for s in catalog['sections'])+1,'questions':questions}
    if existing:catalog['sections'][catalog['sections'].index(existing)]=section
    else:catalog['sections'].append(section)
    catalog['section_count']=len(catalog['sections']);catalog['question_count']=sum(len(s['questions']) for s in catalog['sections'])
    dump(path,catalog)

if __name__=='__main__':build()
