import os, shutil
import spec
from gen import *
import gen
def w(path,txt):
    p=os.path.join(OUT,path); os.makedirs(os.path.dirname(p),exist_ok=True); open(p,'w').write(txt)
def php(v,ind=0):
    pad='    '*ind
    if v is None: return 'null'
    if v is True: return 'true'
    if v is False: return 'false'
    if isinstance(v,(int,float)): return repr(v)
    if isinstance(v,str): return "'"+v.replace('\\','\\\\').replace("'","\\'")+"'"
    if isinstance(v,list):
        if not v: return '[]'
        if all(not isinstance(x,(list,dict)) for x in v): return '['+', '.join(php(x) for x in v)+']'
        return '[\n'+''.join(pad+'    '+php(x,ind+1)+',\n' for x in v)+pad+']'
    if isinstance(v,dict):
        if not v: return '[]'
        return '[\n'+''.join(pad+'    '+php(str(k))+' => '+php(x,ind+1)+',\n' for k,x in v.items())+pad+']'
def rel_name(n):
    b=n[:-3] if n.endswith('_id') else n
    r=camel(b)
    return 'schoolClass' if r=='class' else ('fromSchoolClass' if r=='fromClass' else 'toSchoolClass' if r=='toClass' else r)
def default_lit(c):
    d=c['default']
    if d is None: return None
    if c['type']=='b': return 'true' if d in('1','true') else 'false'
    if c['type'] in('i','d','ll'): return d
    return "'"+d+"'"
# ---------- migrations ----------
def col_line(c):
    n=c['name']; t=c['type']
    if t=='fk':
        s=f"$t->foreignId('{n}')"
        if c['null']: s+='->nullable()'
        if c['uniq']: s+='->unique()'
        s+=f"->constrained('{c['fk']}')"+('->nullOnDelete()' if c['null'] else '->cascadeOnDelete()')
        return s+';'
    base={'s':f"string('{n}')",'f':f"string('{n}')",'t':f"text('{n}')",'i':f"integer('{n}')",'d':f"decimal('{n}', 12, 2)",'ll':f"decimal('{n}', 10, 7)",'b':f"boolean('{n}')",'date':f"date('{n}')",'dt':f"dateTime('{n}')",'time':f"time('{n}')",'j':f"json('{n}')",'ms':f"json('{n}')"}.get(t)
    if t=='e': base=f"enum('{n}', "+php(c['enum'])+")"
    s='$t->'+base
    if c['null']: s+='->nullable()'
    dl=default_lit(c)
    if dl is not None: s+=f'->default({dl})'
    if c['uniq']: s+='->unique()'
    return s+';'
groups={}
for tb,d in TABLES.items(): groups.setdefault(d['mig'],[]).append(tb)
for i,m in enumerate(MIGS):
    tbs=groups.get(m,[])
    if not tbs: continue
    up=''
    for tb in tbs:
        d=TABLES[tb]
        up+=f"        Schema::create('{tb}', function (Blueprint $t) {{\n            $t->id();\n"
        for c in d['cols']: up+='            '+col_line(c)+'\n'
        for u in d['uniques']: up+='            $t->unique('+php(u)+');\n'
        up+="            $t->timestamps();\n        });\n"
    down=''.join(f"        Schema::dropIfExists('{tb}');\n" for tb in reversed(tbs))
    w(f'database/migrations/2025_01_01_{i+10:06d}_create_{m}_tables.php',f"""<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{{
    public function up(): void
    {{
{up}    }}

    public function down(): void
    {{
{down}    }}
}};
""")
w('database/migrations/2025_01_01_000001_add_school_columns_to_users_table.php',"""<?php

use Illuminate\\Database\\Migrations\\Migration;
use Illuminate\\Database\\Schema\\Blueprint;
use Illuminate\\Support\\Facades\\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $t) {
            $t->string('phone')->nullable();
            $t->boolean('is_active')->default(true);
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $t) {
            $t->dropColumn(['phone', 'is_active']);
        });
    }
};
""")
# ---------- models ----------
LAB={'students':"trim($this->first_name.' '.$this->last_name).' · '.$this->admission_no",
'staff':"trim($this->first_name.' '.$this->last_name).' · '.$this->employee_no",
'sections':"optional($this->schoolClass)->name.' – '.$this->name",
'exam_schedules':"optional($this->exam)->name.' · '.optional($this->subject)->name",
'hostel_rooms':"optional($this->hostel)->name.' / '.$this->room_no",
'library_members':"$this->member_no.' · '.(optional($this->student)->label ?: optional($this->staff)->label)",
'fee_structures':"optional($this->schoolClass)->name.' · '.optional($this->feeCategory)->name",
'canteen_wallets':"optional($this->student)->label",
'route_stops':"$this->sequence.'. '.$this->name",
'homework':"$this->title",'online_exam_questions':"\\Illuminate\\Support\\Str::limit($this->question, 60)",
'attendance_credentials':"$this->type.' · '.$this->identifier",
'student_attendances':"'#'.$this->id",
}
def label_expr(tb,cols):
    if tb in LAB: return LAB[tb]
    names={c['name'] for c in cols}
    if 'first_name' in names: return "trim($this->first_name.' '.$this->last_name)"
    for k in ['name','title','child_name','visitor_name','invoice_no','receipt_no','application_no','po_no','sale_no','request_no','asset_tag','member_no','employee_no','registration_no','admission_no','sku','grade','isbn','question','month','identifier']:
        if k in names: return f"$this->{k}"
    return "'#'.$this->id"
inbound={}
for tb,d in TABLES.items():
    seen=set()
    for c in d['cols']:
        if c['type']=='fk' and (tb,c['fk']) not in seen:
            seen.add((tb,c['fk'])); inbound.setdefault(c['fk'],[]).append((tb,c['name']))
for tb,d in TABLES.items():
    cols=d['cols']; M=d['model']
    casts={}
    for c in cols:
        t=c['type']
        if t=='date': casts[c['name']]='date'
        elif t=='dt': casts[c['name']]='datetime'
        elif t=='b': casts[c['name']]='boolean'
        elif t in('j','ms'): casts[c['name']]='array'
        elif t=='d': casts[c['name']]='decimal:2'
        elif t=='ll': casts[c['name']]='decimal:7'
    body=''
    used=set(c['name'] for c in cols)|{'label'}
    for c in cols:
        if c['type']=='fk':
            r=rel_name(c['name'])
            if r in used: continue
            used.add(r)
            body+=f"    public function {r}() {{ return $this->belongsTo({model_of(c['fk'])}::class, '{c['name']}'); }}\n"
    for (src,fkc) in inbound.get(tb,[]):
        r=camel(src)
        if r in used: continue
        used.add(r)
        body+=f"    public function {r}() {{ return $this->hasMany({model_of(src)}::class, '{fkc}'); }}\n"
    ex=d['extra']
    nm={c['name'] for c in cols}
    if 'first_name' in nm and 'name' not in nm and 'getNameAttribute' not in ex: ex=(ex+'\n' if ex else '')+"public function getNameAttribute(){ return trim($this->first_name.' '.$this->last_name); }"
    if ex:
        # drop extras whose method name already exists
        body+='\n'.join('    '+l for l in ex.split('\n'))+'\n'
    w(f'app/Models/{M}.php',f"""<?php

namespace App\\Models;

use Illuminate\\Database\\Eloquent\\Model;

class {M} extends Model
{{
    protected $table = '{tb}';
    protected $guarded = [];
    protected $casts = {php(casts)};

    public function getLabelAttribute() {{ return {label_expr(tb,cols)}; }}
{body}}}
""")
# ---------- resource config ----------
AUTOUSER=['created_by','marked_by','issued_by','reported_by','booked_by','received_by','sold_by','reviewed_by','approved_by','reviewer_id','sender_id']
AUTONUM={'invoice_no':'INV','receipt_no':'RCT','application_no':'APP','po_no':'PO','sale_no':'SL','request_no':'MR','employee_no':'EMP','admission_no':'ADM','member_no':'LIB','asset_tag':'AST','registration_no':None}
AUTOTOK=['api_key','gps_token']
def label_of(n):
    b=n[:-3] if n.endswith('_id') else n
    return {'class':'Class','staff':'Staff member'}.get(b,b.replace('_',' ').capitalize())
cfg={'resources':{},'modules':{}}
for slug,r in RES.items():
    cols=r['cols']; fields=[]
    for c in cols:
        n=c['name']; f=dict(name=n,type=c['type'],label=label_of(n),required=not c['null'] and c['default'] is None and c['type']!='b',default=c['default'],unique=c['uniq'],nullable=c['null'])
        if c['enum']: f['enum']=c['enum']
        if c['type']=='ms': f['enum']=c['enum']
        if c['type']=='fk':
            f['fk']=c['fk']; f['model']=model_of(c['fk']); f['rel']=rel_name(n)
        if n in AUTONUM and AUTONUM[n]: f['auto_number']=AUTONUM[n]; f['required']=False
        if n in AUTOTOK: f['auto_token']=True; f['required']=False
        if n in AUTOUSER: f['auto_user']=True; f['required']=False
        fields.append(f)
    lst=r['list'] or [c['name'] for c in cols if c['type'] not in('t','j','ms','f')][:6]
    srch=r['search'] or [c['name'] for c in cols if c['type']=='s' and not c['enum']][:6]
    if r['fixed']:
        lst=[x for x in lst if x not in r['fixed']]
    cfg['resources'][slug]=dict(table=r['table'],model=model_of(r['table']),label=r['label'],plural=r['plural'],module=r['module'],fields=fields,list=lst,search=srch,fixed=r['fixed'],actions=r['actions'],own=r['own'],notes=r['notes'],
        approve=any(a.get('approve') for a in r['actions']),viewall=bool(r['own']))
for k,m in MODULES.items():
    cfg['modules'][k]=dict(label=m['label'],icon=m['icon'],resources=m['resources'],extras=[dict(label=e[0],route=e[1],params=e[2]) for e in m['extras']])
w('config/school_resources.php',"<?php\n\n// GENERATED from the schema spec. Drives the generic CRUD screens, menus and permissions.\nreturn "+php(cfg)+";\n")
print('models',len(TABLES),'resources',len(RES))
