<?php
declare(strict_types=1);

function osc_stmt(mysqli $con,string $sql):mysqli_stmt{$s=$con->prepare($sql);if(!$s)throw new RuntimeException('Unable to prepare salary configuration operation.');return $s;}
function osc_rows(mysqli $con,string $sql,string $types='',array $params=[]):array{$s=osc_stmt($con,$sql);if($types)$s->bind_param($types,...$params);$s->execute();$rows=[];$r=$s->get_result();while($x=$r->fetch_assoc())$rows[]=$x;$s->close();return $rows;}
function osc_columns(mysqli $con,string $table):array{$out=[];$r=$con->query('SHOW COLUMNS FROM `'.preg_replace('/[^A-Za-z0-9_]/','',$table).'`');if($r)while($x=$r->fetch_assoc())$out[]=$x['Field'];return $out;}
function osc_access_value(string $value):?array{$value=trim($value);if($value===''||$value==='0'||strtolower($value)==='all')return null;preg_match_all('/\d+/',$value,$m);return array_values(array_unique(array_map('intval',$m[0]??[])));}
function osc_allowed_payroll_ids(mysqli $con):?array{$u=current_user()?:[];if((int)($u['role_id']??0)===1)return null;$cols=osc_columns($con,'jos_admin_users');if(!in_array('payroll_access',$cols,true))return [];$s=osc_stmt($con,'SELECT payroll_access FROM jos_admin_users WHERE id=? AND status=1 LIMIT 1');$id=(int)($u['id']??0);$s->bind_param('i',$id);$s->execute();$v=(string)($s->get_result()->fetch_assoc()['payroll_access']??'');$s->close();return osc_access_value($v);}
function osc_payrolls(mysqli $con,?array $allowed):array{$where=['status=1'];$types='';$params=[];if($allowed===[])return [];if(is_array($allowed)){$where[]='id IN ('.implode(',',array_fill(0,count($allowed),'?')).')';$types=str_repeat('i',count($allowed));$params=$allowed;}return osc_rows($con,'SELECT id,name FROM jos_app_payrolllocation WHERE '.implode(' AND ',$where).' ORDER BY name,id',$types,$params);}
function osc_has_id(array $rows,int $id):bool{foreach($rows as $r)if((int)$r['id']===$id)return true;return false;}
function osc_types(mysqli $con):array{return osc_rows($con,'SELECT id,componenttype name FROM jos_app_componenttype ORDER BY id');}
function osc_master_components(mysqli $con,int $type):array{$sql='SELECT id,componenttype,componentname,orderby,attend_config,functionname,status FROM jos_app_salarycomponentmaster WHERE status<>0';$bind='';$params=[];if($type!==6){$sql.=' AND componenttype=?';$bind='i';$params=[$type];}return osc_rows($con,$sql.' ORDER BY orderby ASC,id ASC',$bind,$params);}
function osc_existing(mysqli $con,int $org,int $type):array{$sql='SELECT * FROM jos_app_salaryorganisation WHERE orgid=?';$bind='i';$params=[$org];if($type!==6){$sql.=' AND componenttype=?';$bind.='i';$params[]=$type;}return osc_rows($con,$sql.' ORDER BY orderby ASC,id ASC',$bind,$params);}
function osc_report_rows(mysqli $con,string $table,int $org,int $type):array{$sql='SELECT * FROM '.$table.' WHERE orgid=?';$bind='i';$params=[$org];if($type!==6){$sql.=' AND componenttype=?';$bind.='i';$params[]=$type;}return osc_rows($con,$sql,$bind,$params);}
function osc_map(array $rows,string $key='componentname'):array{$out=[];foreach($rows as $r)$out[(int)$r[$key]]=$r;return $out;}
function osc_ids($value):array{$value=is_array($value)?$value:[];$out=[];foreach($value as $id){$id=(int)$id;if($id>0)$out[$id]=$id;}return array_values($out);}
function osc_cycle(array $graph):bool{$seen=[];$stack=[];$visit=function(int $n)use(&$visit,&$graph,&$seen,&$stack):bool{if(isset($stack[$n]))return true;if(isset($seen[$n]))return false;$seen[$n]=1;$stack[$n]=1;foreach($graph[$n]??[] as $m)if(isset($graph[$m])&&$visit((int)$m))return true;unset($stack[$n]);return false;};foreach(array_keys($graph) as $id)if($visit((int)$id))return true;return false;}
function osc_parse(mysqli $con,array $masters,array $post,array &$errors):array{$valid=[];foreach($masters as $m)$valid[(int)$m['id']]=$m;$rows=[];$graph=[];foreach($valid as $id=>$m){$enabled=isset($post['enabled'][$id]);$add=osc_ids($post['calculation_on'][$id]??[]);$minus=osc_ids($post['calculation_mins'][$id]??[]);foreach(array_merge($add,$minus) as $dep){if($dep===$id)$errors[]=$m['componentname'].' cannot calculate on itself.';elseif(!isset($valid[$dep]))$errors[]='Invalid calculation component selected for '.$m['componentname'].'.';}if(count($add)!==count(array_unique($add))||count($minus)!==count(array_unique($minus)))$errors[]='Duplicate calculation mapping found for '.$m['componentname'].'.';$order=trim((string)($post['orderby'][$id]??''));$ctcOrder=trim((string)($post['ctc_order'][$id]??''));$dateOrder=trim((string)($post['date_order'][$id]??''));$musterOrder=trim((string)($post['muster_order'][$id]??''));foreach([[$order,'Order'],[$ctcOrder,'CTC order'],[$dateOrder,'Date-wise order'],[$musterOrder,'Muster order']] as [$v,$label])if($v!==''&&(!ctype_digit($v)||(int)$v>9999))$errors[]=$label.' must be between 0 and 9999 for '.$m['componentname'].'.';$rows[$id]=['id'=>$id,'componenttype'=>(int)$m['componenttype'],'enabled'=>$enabled,'orderby'=>$order===''?0:(int)$order,'color'=>isset($post['color'][$id])?1:0,'calculation_on'=>$add,'calculation_mins'=>$minus,'onlycalculate'=>isset($post['onlycalculate'][$id])?1:0,'ctc'=>isset($post['ctc'][$id])?1:0,'ctc_order'=>$ctcOrder===''?0:(int)$ctcOrder,'datereport'=>isset($post['datereport'][$id])?1:0,'date_order'=>$dateOrder===''?0:(int)$dateOrder,'musterreport'=>isset($post['musterreport'][$id])?1:0,'muster_order'=>$musterOrder===''?0:(int)$musterOrder];if($enabled)$graph[$id]=array_values(array_unique(array_merge($add,$minus)));}if(osc_cycle($graph))$errors[]='Calculation mappings contain a circular dependency.';return $rows;}
function osc_delete_scope(mysqli $con,string $table,int $org,int $type):void{$sql='DELETE FROM '.$table.' WHERE orgid=?';$bind='i';$params=[$org];if($type!==6){$sql.=' AND componenttype=?';$bind.='i';$params[]=$type;}$s=osc_stmt($con,$sql);$s->bind_param($bind,...$params);$s->execute();$s->close();}
function osc_save(mysqli $con,int $org,int $type,array $rows):void{$uid=(int)(current_user()['id']??0);$con->begin_transaction();try{foreach(['jos_app_salaryorganisation','jos_app_ctcreport','jos_app_datewisereport'] as $t)osc_delete_scope($con,$t,$org,$type);foreach($rows as $r){$add=implode(',',$r['calculation_on']);$minus=implode(',',$r['calculation_mins']);if($r['enabled']){$s=osc_stmt($con,'INSERT INTO jos_app_salaryorganisation (date,sysdate,orgid,componenttype,componentname,orderby,color,calculation_on,calculation_mins,onlycalculate) VALUES (CURDATE(),NOW(),?,?,?,?,?,?,?,?)');$s->bind_param('iiiiissi',$org,$r['componenttype'],$r['id'],$r['orderby'],$r['color'],$add,$minus,$r['onlycalculate']);$s->execute();$salid=$s->insert_id;$s->close();$s=osc_stmt($con,'INSERT INTO jos_app_salaryorganisation_editlog (date,salid,orgid,componenttype,componentname,orderby,calculation_on,calculation_mins,onlycalculate,modify_by) VALUES (NOW(),?,?,?,?,?,?,?,?,?)');$s->bind_param('iiiiissii',$salid,$org,$r['componenttype'],$r['id'],$r['orderby'],$add,$minus,$r['onlycalculate'],$uid);$s->execute();$s->close();}if($r['ctc']){$s=osc_stmt($con,'INSERT INTO jos_app_ctcreport (date,sysdate,orgid,componenttype,componentname,orderby,color,ctc,calculation_on,calculation_mins) VALUES (CURDATE(),NOW(),?,?,?,?,?,?,?,?)');$s->bind_param('iiiiiiss',$org,$r['componenttype'],$r['id'],$r['ctc_order'],$r['color'],$r['ctc'],$add,$minus);$s->execute();$s->close();}if($r['datereport']||$r['musterreport']){$s=osc_stmt($con,'INSERT INTO jos_app_datewisereport (date,sysdate,orgid,componenttype,componentname,orderby,musterorderby,color,datereport,musterreport,calculation_on,calculation_mins) VALUES (CURDATE(),NOW(),?,?,?,?,?,?,?,?,?,?)');$s->bind_param('iiiiiiiiss',$org,$r['componenttype'],$r['id'],$r['date_order'],$r['muster_order'],$r['color'],$r['datereport'],$r['musterreport'],$add,$minus);$s->execute();$s->close();}}$con->commit();}catch(Throwable $e){$con->rollback();throw $e;}}
function osc_copy(mysqli $con,int $from,int $to):int{$source=osc_rows($con,'SELECT componenttype,componentname,orderby,calculation_on,calculation_mins,color FROM jos_app_salaryorganisation WHERE orgid=? ORDER BY id','i',[$from]);if(!$source)throw new DomainException('The source organisation has no salary configuration.');$exists=osc_rows($con,'SELECT id FROM jos_app_salaryorganisation WHERE orgid=? LIMIT 1','i',[$to]);if($exists)throw new DomainException('The destination organisation already has salary configuration.');$con->begin_transaction();try{$count=0;foreach($source as $r){$s=osc_stmt($con,'INSERT INTO jos_app_salaryorganisation (orgid,componenttype,componentname,orderby,calculation_on,calculation_mins,color) VALUES (?,?,?,?,?,?,?)');$s->bind_param('iiiissi',$to,$r['componenttype'],$r['componentname'],$r['orderby'],$r['calculation_on'],$r['calculation_mins'],$r['color']);$s->execute();$s->close();$count++;}$con->commit();return$count;}catch(Throwable $e){$con->rollback();throw$e;}}
