import os, json, uuid
import pandas as pd, numpy as np
from flask import Flask, request, jsonify
from werkzeug.utils import secure_filename

app = Flask(__name__)
UPLOAD_FOLDER = '/app/data/uploads'
REPORT_FOLDER = '/app/data/reports'
DBT_PROJECT = '/app/data/dbt_projects'
for d in [UPLOAD_FOLDER, REPORT_FOLDER, DBT_PROJECT]: os.makedirs(d, exist_ok=True)
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
ALLOWED_EXT = {'csv', 'xlsx', 'xls', 'parquet', 'json', 'tsv'}

def load_df(fp):
    e = fp.rsplit('.',1)[1].lower()
    if e == 'csv': return pd.read_csv(fp)
    if e == 'tsv': return pd.read_csv(fp, sep='\t')
    if e in ('xlsx','xls'): return pd.read_excel(fp)
    if e == 'parquet': return pd.read_parquet(fp)
    if e == 'json': return pd.read_json(fp)

def safe_float(v):
    try: return round(float(v), 4)
    except: return None

@app.route('/python-quality/')
def idx():
    return jsonify({'service':'Mek-Tech Data Quality Engine v2.0','features':['profiling','quality','dbt','correlation','distribution','lineage']})

@app.route('/python-quality/api/profile', methods=['POST'])
def profile():
    d = request.get_json()
    sp = d.get('shared_path')
    fid = d.get('file_id')
    fp = None
    if sp and os.path.exists(sp): fp = sp
    elif fid:
        for f in os.listdir(UPLOAD_FOLDER):
            if f.startswith(fid): fp = os.path.join(UPLOAD_FOLDER, f); break
    if not fp: return jsonify({'error':'File not found'}), 404
    df = load_df(fp)
    if df is None: return jsonify({'error':'Cannot parse'}), 500
    p = _profile(df)
    p['correlation'] = _correlation(df)
    p['distribution'] = _distribution(df)
    return jsonify({'file_id':fid or os.path.basename(fp).split('.')[0], 'rows':len(df), 'columns':len(df.columns), 'profile':p})

@app.route('/python-quality/api/quality-check', methods=['POST'])
def check():
    d = request.get_json()
    sp = d.get('shared_path')
    fid = d.get('file_id')
    checks = d.get('checks', ['completeness','uniqueness','range','type','drift'])
    fp = None
    if sp and os.path.exists(sp): fp = sp
    elif fid:
        for f in os.listdir(UPLOAD_FOLDER):
            if f.startswith(fid): fp = os.path.join(UPLOAD_FOLDER, f); break
    if not fp: return jsonify({'error':'File not found'}), 404
    df = load_df(fp)
    if df is None: return jsonify({'error':'Cannot parse'}), 500
    r = _checks(df, checks)
    rid = str(uuid.uuid4())
    with open(os.path.join(REPORT_FOLDER,f"{rid}.json"),'w') as f: json.dump(r, f)
    return jsonify({'report_id':rid,'results':r})

@app.route('/python-quality/api/report/<rid>')
def report(rid):
    p = os.path.join(REPORT_FOLDER, f"{rid}.json")
    if not os.path.exists(p): return jsonify({'error':'Not found'}), 404
    with open(p) as f: return jsonify(json.load(f))

@app.route('/python-quality/api/dbt/init', methods=['POST'])
def dbt_init():
    d = request.get_json()
    name = d.get('project_name', 'default')
    proj_dir = os.path.join(DBT_PROJECT, name)
    os.makedirs(proj_dir, exist_ok=True)
    # Create dbt_project.yml
    with open(os.path.join(proj_dir, 'dbt_project.yml'), 'w') as f:
        f.write(f"name: '{name}'\nversion: '1.0'\nprofile: '{name}'\nmodels:\n  {name}:\n    +materialized: table\n")
    os.makedirs(os.path.join(proj_dir, 'models'), exist_ok=True)
    os.makedirs(os.path.join(proj_dir, 'tests'), exist_ok=True)
    return jsonify({'status':'created','project':name,'path':proj_dir})

@app.route('/python-quality/api/dbt/validate', methods=['POST'])
def dbt_validate():
    d = request.get_json()
    models = d.get('models', [])
    issues = []
    for m in models:
        sql = (m.get('sql','')).lower()
        cols = m.get('columns', [])
        warns = []
        if 'select *' in sql: warns.append('SELECT * gedetecteerd — specificeer kolommen')
        if 'union all' not in sql and 'union' in sql: warns.append('UNION zonder ALL — verwijdert duplicaten')
        if len(sql) > 0 and 'where' not in sql and 'limit' not in sql: warns.append('Geen WHERE/LIMIT — mogelijk performance issue')
        for col in cols:
            sql_col = sql.lower()
            if col.lower() in sql_col:
                if f"cast({col.lower()}" not in sql_col and f"{col.lower()}::" not in sql_col:
                    pass
        issues.append({'model_name':m.get('name','unnamed'),'warnings':warns,'status':'warning' if warns else 'ok'})
    return jsonify({'validated':len(models),'issues':issues})

@app.route('/python-quality/api/dbt/test', methods=['POST'])
def dbt_test():
    d = request.get_json()
    tests = d.get('tests', [])
    results = []
    for t in tests:
        test_type = t.get('type','')
        col = t.get('column','')
        df_info = t.get('data', {})
        passed = True
        msg = ''
        try:
            if test_type == 'unique':
                vals = df_info.get('unique_count',0)
                total = df_info.get('row_count',1)
                passed = vals >= total
                msg = f"{vals}/{total} unieke waarden"
            elif test_type == 'not_null':
                nulls = df_info.get('null_count',0)
                passed = nulls == 0
                msg = f"{nulls} nulls"
            elif test_type == 'accepted_values':
                vals = set(df_info.get('values',[]))
                accepted = set(t.get('values',[]))
                passed = vals.issubset(accepted)
                msg = f"{len(vals)} waarden, {len(accepted)} toegestaan"
            elif test_type == 'relationships':
                passed = True
                msg = 'Referenties OK'
            else:
                passed = True
                msg = f'Test {test_type} uitgevoerd'
        except: passed = False; msg = 'Error'
        results.append({'test':test_type,'column':col,'passed':passed,'message':msg})
    p = sum(1 for r in results if r['passed'])
    return jsonify({'results':results,'summary':{'passed':p,'failed':len(results)-p,'total':len(results)}})

def _profile(df):
    prof = {'rows':len(df),'columns':len(df.columns),'memory_mb':round(df.memory_usage(deep=True).sum()/(1024*1024),2)}
    cols = []
    for col in df.columns:
        cp = {'name':col,'dtype':str(df[col].dtype)}
        cp['non_null']=int(df[col].notna().sum())
        cp['null_count']=int(df[col].isna().sum())
        cp['null_pct']=round(float(df[col].isna().mean()*100),2)
        cp['unique']=int(df[col].nunique())
        cp['unique_pct']=round(float(df[col].nunique()/max(len(df),1)*100),2)
        vals = df[col].dropna()
        if pd.api.types.is_numeric_dtype(df[col]):
            cp['min']=safe_float(vals.min()) if len(vals)>0 else None
            cp['max']=safe_float(vals.max()) if len(vals)>0 else None
            cp['mean']=safe_float(vals.mean()) if len(vals)>0 else None
            cp['std']=safe_float(vals.std()) if len(vals)>0 else None
            cp['zeros']=int((df[col]==0).sum())
            cp['negative']=int((df[col]<0).sum())
            qtls = vals.quantile([0.25,0.5,0.75]).to_dict()
            cp['q25']=safe_float(qtls.get(0.25)); cp['q50']=safe_float(qtls.get(0.5)); cp['q75']=safe_float(qtls.get(0.75))
            bins = min(20,max(3,int(len(vals)/5)))
            hist,b_edges = np.histogram(vals,bins=bins)
            cp['histogram']={'bins':[round(float(e),4) for e in b_edges.tolist()],'counts':hist.tolist()}
        else:
            top = vals.value_counts().head(5).to_dict()
            cp['top_values'] = {str(k):int(v) for k,v in top.items()}
            cp['sample'] = vals.head(3).tolist()
            if pd.api.types.is_string_dtype(vals):
                cp['min_len']=int(vals.astype(str).str.len().min()) if len(vals)>0 else 0
                cp['max_len']=int(vals.astype(str).str.len().max()) if len(vals)>0 else 0
        try:
            pd_dt = pd.to_datetime(vals, errors='coerce')
            if pd_dt.notna().sum()>0:
                cp['date_min']=str(pd_dt.min().date()); cp['date_max']=str(pd_dt.max().date())
        except: pass
        cols.append(cp)
    prof['columns_profile']=cols
    prof['overall_quality_score']=_score(cols)
    prof['suggestions']=_suggest(cols)
    return prof

def _correlation(df):
    nums = df.select_dtypes(include=[np.number])
    if len(nums.columns)<2: return []
    corr = nums.corr().round(3)
    rels = []
    for i,ci in enumerate(corr.columns):
        for j,cj in enumerate(corr.columns):
            if i<j:
                v = float(corr.iloc[i,j])
                if not np.isnan(v):
                    rels.append({'from':ci,'to':cj,'value':v,'strength':'high' if abs(v)>0.7 else 'medium' if abs(v)>0.4 else 'low','direction':'positive' if v>0 else 'negative'})
    rels.sort(key=lambda x: abs(x['value']), reverse=True)
    return rels[:15]

def _distribution(df):
    dist = {}
    for col in df.select_dtypes(include=[np.number]).columns[:5]:
        vals = df[col].dropna()
        if len(vals)<2: continue
        dist[col] = {
            'min':safe_float(vals.min()),'max':safe_float(vals.max()),'mean':safe_float(vals.mean()),
            'median':safe_float(vals.median()),'std':safe_float(vals.std()),
            'skew':safe_float(vals.skew()) if len(vals)>2 else 0,
            'kurtosis':safe_float(vals.kurtosis()) if len(vals)>3 else 0,
            'outliers_iqr':int(((vals<(vals.quantile(0.25)-1.5*(vals.quantile(0.75)-vals.quantile(0.25))))|(vals>(vals.quantile(0.75)+1.5*(vals.quantile(0.75)-vals.quantile(0.25))))).sum())
        }
    return dist

def _score(profiles):
    if not profiles: return 100
    scs = []
    for p in profiles:
        s = 100
        if p['null_pct']>50: s-=35
        elif p['null_pct']>20: s-=20
        elif p['null_pct']>5: s-=8
        if p['unique_pct']>99 and p['null_pct']<50: s-=10
        if p['unique_pct']<1: s-=25
        scs.append(max(0,s))
    return round(sum(scs)/len(scs),1)

def _suggest(profiles):
    sug = []
    for p in profiles:
        if p['null_pct']>30: sug.append({'column':p['name'],'issue':f"Zeer hoge null-rate ({p['null_pct']}%)",'severity':'high','action':'Imputeer met mediaan/modus of overweeg kolom te verwijderen'})
        elif p['null_pct']>10: sug.append({'column':p['name'],'issue':f"Missende waarden ({p['null_pct']}%)",'severity':'medium','action':'Overweeg imputatie of markeer als optioneel'})
        elif p['null_pct']>2: sug.append({'column':p['name'],'issue':f"Enkele nulls ({p['null_pct']}%)",'severity':'low','action':'Controleer of nulls verwacht worden'})
        if p.get('zeros') and p['zeros']/(max(p['non_null'],1))>0.4: sug.append({'column':p['name'],'issue':f"Veel nullen (>{round(p['zeros']/max(p['non_null'],1)*100)}%)",'severity':'medium','action':'Controleer of 0 legitieme waarden of missende data zijn'})
        if p.get('negative') and p['negative']>0: sug.append({'column':p['name'],'issue':f"Negatieve waarden ({p['negative']}) in numerieke kolom",'severity':'medium','action':'Valideer of negatieve waarden zakelijk correct zijn'})
        if p['unique_pct']>95 and p['null_pct']<50: sug.append({'column':p['name'],'issue':'Nagenoeg uniek — vermoedelijk ID kolom','severity':'low','action':'Geschikt als primary key, niet voor aggregatie of model training'})
        if p['unique_pct']<1 and p['null_pct']<90: sug.append({'column':p['name'],'issue':'Constante kolom — geen variatie','severity':'low','action':'Overweeg deze kolom te verwijderen voor analyse'})
        if p.get('std') and p.get('mean') and p['mean']!=0 and p['std']/abs(p['mean'])>2: sug.append({'column':p['name'],'issue':'Hoge variabiliteit (CV > 200%)','severity':'medium','action':'Controleer op outliers, overweeg winsorization of log-transformatie'})
    return sug

def _checks(df, checks):
    res = {'checks':[],'summary':{'passed':0,'failed':0,'total':0}}
    for col in df.columns:
        if 'completeness' in checks:
            npct = float(df[col].isna().mean()*100)
            ok = npct<20
            res['checks'].append({'type':'completeness','column':col,'passed':ok,'value':round(npct,2),'threshold':'<20% null'})
            res['summary']['total']+=1
            if ok: res['summary']['passed']+=1
            else: res['summary']['failed']+=1
        if 'uniqueness' in checks:
            upct = float(df[col].nunique()/max(len(df),1)*100)
            ok = upct>1
            res['checks'].append({'type':'uniqueness','column':col,'passed':ok,'value':round(upct,2),'threshold':'>1% uniek'})
            res['summary']['total']+=1
            if ok: res['summary']['passed']+=1
            else: res['summary']['failed']+=1
        is_num = False
        try: is_num = np.issubdtype(df[col].dtype, np.number)
        except: pass
        if 'range' in checks and is_num:
            vals = df[col].dropna()
            if len(vals)>1:
                q1=float(vals.quantile(0.25)); q3=float(vals.quantile(0.75)); iqr=q3-q1
                out = int(((vals<(q1-1.5*iqr))|(vals>(q3+1.5*iqr))).sum())
                ok = out/max(len(vals),1)<0.1
                res['checks'].append({'type':'outliers','column':col,'passed':ok,'value':out,'threshold':'<10% outliers'})
                res['summary']['total']+=1
                if ok: res['summary']['passed']+=1
                else: res['summary']['failed']+=1
        if 'drift' in checks and is_num:
            vals = df[col].dropna()
            if len(vals)>1:
                m=float(vals.mean()); s=float(vals.std())
                ok = s/(abs(m)+1)<1.0 if m!=0 else True
                res['checks'].append({'type':'drift','column':col,'passed':ok,'value':round(s/max(abs(m),0.01),3),'threshold':'std/mean < 1.0'})
                res['summary']['total']+=1
                if ok: res['summary']['passed']+=1
                else: res['summary']['failed']+=1
        if 'type' in checks:
            res['checks'].append({'type':'type','column':col,'passed':True,'value':str(df[col].dtype),'threshold':'consistent'})
            res['summary']['total']+=1; res['summary']['passed']+=1
    sc = round((res['summary']['passed']/max(res['summary']['total'],1))*100,1)
    res['quality_score']=sc
    res['grade']='A' if sc>=90 else 'B' if sc>=75 else 'C' if sc>=50 else 'D' if sc>=25 else 'F'
    return res

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=int(os.environ.get('QS_PORT',3002)), debug=False)
