#!/usr/bin/env python3
"""Delta-Sierra V20.9.2 — cautious AMECO UBLGAPS extractor.

Usage:
  python import-ameco-ublgaps-v2092.py SOURCE --release-date YYYY-MM-DD --release-name "..." --out OUTPUT.csv
  python import-ameco-ublgaps-v2092.py --self-test

The program deliberately fails closed: if it cannot identify France + UBLGAPS + annual periods unambiguously, it writes nothing.
"""
from __future__ import annotations
import argparse,csv,hashlib,io,json,re,sys,zipfile
from pathlib import Path
TARGET_VAR='UBLGAPS'; TARGET_COUNTRY='FRA'
YEAR=re.compile(r'^(19|20)\d{2}$')

def sha256(p):
 h=hashlib.sha256();
 with open(p,'rb') as f:
  for b in iter(lambda:f.read(1024*1024),b''): h.update(b)
 return h.hexdigest()

def members(source:Path):
 if source.is_dir():
  for p in source.rglob('*'):
   if p.is_file() and p.suffix.lower() in {'.csv','.txt','.tsv','.dat'}: yield str(p.relative_to(source)),p.read_bytes()
 elif zipfile.is_zipfile(source):
  with zipfile.ZipFile(source) as z:
   for n in z.namelist():
    if Path(n).suffix.lower() in {'.csv','.txt','.tsv','.dat'}: yield n,z.read(n)
 else: yield source.name,source.read_bytes()

def decode(b):
 for enc in ('utf-8-sig','utf-8','cp1252','latin-1'):
  try:return b.decode(enc)
  except UnicodeDecodeError: pass
 raise ValueError('undecodable text')

def delim(line):
 scores={d:line.count(d) for d in (';','\\t',',','|')}; return max(scores,key=scores.get)

def parse_numeric(x):
 x=x.strip().replace(' ','').replace(',','.')
 if x in {'','NA','N/A','na','n/a',':','..','.'}: return None
 return float(x)

def extract_wide(text,member):
 lines=[l for l in text.splitlines() if l.strip()]
 for i,line in enumerate(lines[:80]):
  d=delim(line); head=next(csv.reader([line],delimiter=d))
  years=[(j,h.strip()) for j,h in enumerate(head) if YEAR.match(h.strip())]
  if len(years)<3: continue
  for row in csv.reader(lines[i+1:],delimiter=d):
   blob=' '.join(row).upper()
   if TARGET_VAR not in blob or TARGET_COUNTRY not in blob: continue
   obs=[]
   for j,y in years:
    if j<len(row):
     try:v=parse_numeric(row[j])
     except: v=None
     if v is not None: obs.append((int(y),v))
   if obs:return obs
 return []

def extract_long(text,member):
 lines=[l for l in text.splitlines() if l.strip()]
 for i,line in enumerate(lines[:80]):
  d=delim(line); head=[h.strip().lower() for h in next(csv.reader([line],delimiter=d))]
  def ix(names):
   for n in names:
    if n in head:return head.index(n)
   return None
  yi=ix(['year','period','time']); vi=ix(['value','obs_value','observation']); ci=ix(['country','geo','country_code']); si=ix(['variable','series','code','indicator'])
  if yi is None or vi is None: continue
  obs=[]
  for row in csv.reader(lines[i+1:],delimiter=d):
   if max(yi,vi,ci or 0,si or 0)>=len(row):continue
   blob=' '.join(row).upper()
   if TARGET_VAR not in blob or TARGET_COUNTRY not in blob: continue
   y=row[yi].strip()
   if not YEAR.match(y):continue
   try:v=parse_numeric(row[vi])
   except:continue
   if v is not None:obs.append((int(y),v))
  if obs:return sorted(set(obs))
 return []

def extract(source):
 candidates=[]
 for name,b in members(source):
  text=decode(b)
  if TARGET_VAR not in text.upper() or TARGET_COUNTRY not in text.upper():continue
  obs=extract_wide(text,name) or extract_long(text,name)
  if obs:candidates.append((name,obs))
 if len(candidates)!=1:
  raise RuntimeError(f'Fail-closed: expected exactly one unambiguous candidate, found {len(candidates)}: {[n for n,_ in candidates]}')
 return candidates[0]

def write(source,release_date,release_name,out):
 name,obs=extract(source); digest=sha256(source) if source.is_file() else None
 with open(out,'w',newline='',encoding='utf-8') as f:
  w=csv.writer(f,delimiter=';'); w.writerow(['release_date','release_name','source_member','source_sha256','country','series','year','value','unit'])
  for y,v in obs:w.writerow([release_date,release_name,name,digest or '',TARGET_COUNTRY,TARGET_VAR,y,v,'percent_potential_GDP_current_prices'])
 return len(obs)

def selftest():
 import tempfile
 sample='COUNTRY;VARIABLE;UNIT;2022;2023;2024\nFRA;UBLGAPS;319;-4.1;-4.3;-4.5\nDEU;UBLGAPS;319;-2;-2;-2\n'
 with tempfile.TemporaryDirectory() as td:
  p=Path(td)/'fixture.csv';p.write_text(sample,encoding='utf-8')
  n,obs=extract(p)
  assert obs==[(2022,-4.1),(2023,-4.3),(2024,-4.5)]
 print('SELF-TEST PASS — synthetic fixture only; no claim about AMECO observations.')

def main():
 ap=argparse.ArgumentParser();ap.add_argument('source',nargs='?');ap.add_argument('--release-date');ap.add_argument('--release-name');ap.add_argument('--out');ap.add_argument('--self-test',action='store_true');a=ap.parse_args()
 if a.self_test:return selftest()
 if not all([a.source,a.release_date,a.release_name,a.out]):ap.error('source, --release-date, --release-name and --out are required')
 p=Path(a.source); n=write(p,a.release_date,a.release_name,a.out);print(f'Wrote {n} observations to {a.out}')
if __name__=='__main__':main()
