<?php

require_once __DIR__ . '/../finance/FinanceCenterAssignmentService.php';

class CostManagementService
{
    private $pdo;
    private $centerAssignment;

    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
        $this->assertInstalled();
        $this->centerAssignment = new FinanceCenterAssignmentService($pdo);
    }

    public function normalizePeriod($period)
    {
        $period = trim((string)$period);
        if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period)) {
            throw new InvalidArgumentException('El período debe usar el formato AAAA-MM.');
        }
        return $period;
    }

    public function getWorkspace($period, $includePayroll)
    {
        $period = $this->normalizePeriod($period);
        $entries = $this->entries($period, $includePayroll);
        $entryIds = array_map(function ($row) { return (int)$row['id']; }, $entries);
        $allocations = $this->allocationsByEntry($entryIds);
        foreach ($entries as &$entry) {
            $entry['allocations'] = isset($allocations[(int)$entry['id']]) ? $allocations[(int)$entry['id']] : array();
        }
        unset($entry);

        $summary = $this->summary($period);
        if (!$includePayroll) unset($summary['payroll_total']);
        $summary['unclassified_count'] = 0;
        foreach ($entries as $entry) {
            if (empty($entry['category_id']) || (string)$entry['category_code'] === 'OTR') $summary['unclassified_count']++;
        }

        $close = $this->periodClose($period);
        return array(
            'period' => $period,
            'is_closed' => $close && $close['close_status'] === 'closed',
            'close' => $close,
            'summary' => $summary,
            'entries' => $entries,
            'categories' => $this->categories(false),
            'centers' => $this->centers(),
            'suppliers' => $this->suppliers(),
            'recurring_rules' => $this->recurringRules(),
            'payroll' => $includePayroll ? $this->payroll($period) : array(),
            'charts' => array(
                'by_category' => $this->groupSummary($period, 'category'),
                'by_nature' => $this->groupSummary($period, 'nature'),
                'by_center' => $this->groupSummary($period, 'center'),
            ),
            'available_dte_count' => $this->availableDteCount($period),
            'assignment_policy' => $this->centerAssignment->settings(),
        );
    }

    public function saveEntry(array $data, $userId, $canApprove)
    {
        $id = isset($data['id']) ? (int)$data['id'] : 0;
        $existing = $id > 0 ? $this->entryForUpdate($id) : null;
        if ($id > 0 && !$existing) throw new RuntimeException('El costo ya no existe.');
        if ($existing && $existing['source_type'] === 'payroll') throw new RuntimeException('La remuneración se edita desde su sección protegida.');
        if ($existing && $existing['source_type'] === 'dte') {
            // El documento de origen conserva fechas, contraparte, montos y estado.
            // Desde Costos sólo se clasifica, define IVA de gestión y distribuye por centros.
            $data['entry_date'] = $existing['entry_date'];
            $data['due_date'] = $existing['due_date'];
            $data['payment_date'] = $existing['payment_date'];
            $data['description'] = $existing['description'];
            $data['supplier_id'] = $existing['supplier_id'];
            $data['supplier_name'] = $existing['supplier_name'];
            $data['net_amount'] = $existing['net_amount'];
            $data['tax_amount'] = $existing['tax_amount'];
            $data['gross_amount'] = $existing['gross_amount'];
            $data['status'] = $existing['status'];
        }

        $entryDate = $this->dateValue($data['entry_date'] ?? '', true, 'Fecha del costo');
        $period = $this->normalizePeriod(substr($entryDate, 0, 7));
        $this->assertPeriodOpen($period);
        if ($existing && $existing['period'] !== $period) $this->assertPeriodOpen($existing['period']);

        $categoryId = (int)($data['category_id'] ?? 0);
        $category = $this->category($categoryId);
        if (!$category || (!(int)$category['is_active'] && (!$existing || (int)$existing['category_id'] !== $categoryId))) throw new InvalidArgumentException('Selecciona una categoría de costo activa.');

        $description = trim((string)($data['description'] ?? ''));
        if ($description === '' || mb_strlen($description) > 255) throw new InvalidArgumentException('Ingresa una descripción de hasta 255 caracteres.');
        $ivaTreatment = trim((string)($data['iva_treatment'] ?? $category['default_iva_treatment']));
        if (!in_array($ivaTreatment, array('recoverable','nonrecoverable','no_iva'), true)) throw new InvalidArgumentException('Tratamiento de IVA no válido.');

        $net = $this->money($data['net_amount'] ?? 0);
        $tax = $this->money($data['tax_amount'] ?? 0);
        $gross = $this->money($data['gross_amount'] ?? ($net + $tax));
        if ($gross <= 0 && ($net > 0 || $tax > 0)) $gross = $net + $tax;
        if ($net < 0 || $tax < 0 || $gross < 0 || $gross + 0.02 < $net) throw new InvalidArgumentException('Los montos del costo no son válidos.');
        $management = $ivaTreatment === 'recoverable' ? $net : $gross;

        $status = trim((string)($data['status'] ?? ($existing ? $existing['status'] : 'draft')));
        if (!in_array($status, array('draft','approved','paid'), true)) $status = 'draft';
        if (!$canApprove && in_array($status, array('approved','paid'), true) && (!$existing || $existing['status'] === 'draft')) {
            $status = 'draft';
        }
        $paymentDate = $this->dateValue($data['payment_date'] ?? '', false, 'Fecha de pago');
        if ($status === 'paid' && !$paymentDate) throw new InvalidArgumentException('Un costo pagado requiere fecha de pago.');

        $supplierId = (int)($data['supplier_id'] ?? 0);
        $supplier = $supplierId > 0 ? $this->supplier($supplierId) : null;
        if ($supplierId > 0 && !$supplier && !($existing && $existing['source_type'] === 'dte' && (int)$existing['supplier_id'] === $supplierId)) throw new InvalidArgumentException('El proveedor seleccionado no es válido.');
        $supplierName = $supplier ? $supplier['display_name'] : trim((string)($data['supplier_name'] ?? ''));
        $primaryCenter = (int)($data['finance_center_id'] ?? 0);
        $allocations = $this->normalizeAllocations($data['allocations'] ?? array(), $primaryCenter, $management);
        $assignment = array('source' => 'manual', 'source_label' => 'Asignación manual');
        if (!$primaryCenter && !$allocations) {
            $context = $existing && $existing['source_type'] === 'dte' ? 'dte' : 'manual';
            $assignment = $this->centerAssignment->resolveCostCenter($supplierId, $categoryId, 0, $context);
            if (!empty($assignment['center_id'])) {
                $primaryCenter = (int)$assignment['center_id'];
                $allocations = $this->normalizeAllocations(array(), $primaryCenter, $management);
            }
        }
        if (!$primaryCenter && $allocations) $primaryCenter = (int)$allocations[0]['finance_center_id'];

        $before = $existing;
        $this->pdo->beginTransaction();
        try {
            if ($existing) {
                $stmt = $this->pdo->prepare("UPDATE cost_entries SET period=?,entry_date=?,due_date=?,payment_date=?,category_id=?,nature=?,description=?,supplier_id=?,supplier_name=?,finance_center_id=?,iva_treatment=?,net_amount=?,tax_amount=?,gross_amount=?,management_cost_amount=?,status=?,notes=?,approved_by=CASE WHEN ? IN ('approved','paid') AND approved_at IS NULL THEN ? ELSE approved_by END,approved_at=CASE WHEN ? IN ('approved','paid') AND approved_at IS NULL THEN NOW() ELSE approved_at END,updated_by=?,updated_at=NOW() WHERE id=?");
                $stmt->execute(array($period,$entryDate,$this->dateValue($data['due_date'] ?? '',false,'Fecha de vencimiento'),$paymentDate,$categoryId,$category['nature'],$description,$supplierId ?: null,$supplierName ?: null,$primaryCenter ?: null,$ivaTreatment,$net,$tax,$gross,$management,$status,trim((string)($data['notes'] ?? '')),$status,$userId,$status,$userId,$id));
            } else {
                $stmt = $this->pdo->prepare("INSERT INTO cost_entries (period,entry_date,due_date,payment_date,source_type,category_id,nature,description,supplier_id,supplier_name,finance_center_id,iva_treatment,net_amount,tax_amount,gross_amount,management_cost_amount,status,notes,approved_by,approved_at,created_by,updated_by) VALUES (?,?,?,?,'manual',?,?,?,?,?,?,?,?,?,?,?,?,?,CASE WHEN ? IN ('approved','paid') THEN ? ELSE NULL END,CASE WHEN ? IN ('approved','paid') THEN NOW() ELSE NULL END,?,?)");
                $stmt->execute(array($period,$entryDate,$this->dateValue($data['due_date'] ?? '',false,'Fecha de vencimiento'),$paymentDate,$categoryId,$category['nature'],$description,$supplierId ?: null,$supplierName ?: null,$primaryCenter ?: null,$ivaTreatment,$net,$tax,$gross,$management,$status,trim((string)($data['notes'] ?? '')),$status,$userId,$status,$userId,$userId));
                $id = (int)$this->pdo->lastInsertId();
            }
            $this->replaceAllocations($id, $allocations);
            $after = $this->entryForUpdate($id);
            $this->audit('cost_entry', $id, $existing ? 'updated' : 'created', $period, $before, $after, $userId);
            $this->pdo->commit();
            return array('id' => $id, 'period' => $period, 'center_assignment' => $assignment);
        } catch (Throwable $e) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $e;
        }
    }

    public function voidEntry($id, $userId)
    {
        $entry = $this->entryForUpdate((int)$id);
        if (!$entry) throw new RuntimeException('El costo ya no existe.');
        if ($entry['source_type'] !== 'manual' && $entry['source_type'] !== 'recurring') {
            throw new RuntimeException('Los costos vinculados se anulan desde su documento de origen.');
        }
        $this->assertPeriodOpen($entry['period']);
        $stmt = $this->pdo->prepare("UPDATE cost_entries SET status='void',updated_by=?,updated_at=NOW() WHERE id=?");
        $stmt->execute(array($userId,(int)$id));
        $this->audit('cost_entry',(int)$id,'voided',$entry['period'],$entry,array('status'=>'void'),$userId);
    }

    public function saveCategory(array $data, $userId)
    {
        $id = (int)($data['id'] ?? 0);
        $code = strtoupper(trim((string)($data['category_code'] ?? '')));
        $name = trim((string)($data['category_name'] ?? ''));
        $nature = trim((string)($data['nature'] ?? 'fixed'));
        $group = trim((string)($data['category_group'] ?? 'other'));
        $iva = trim((string)($data['default_iva_treatment'] ?? 'recoverable'));
        $cash = trim((string)($data['cash_activity'] ?? 'operations'));
        if (!preg_match('/^[A-Z0-9_-]{2,30}$/',$code) || $name === '') throw new InvalidArgumentException('Código o nombre de categoría no válido.');
        if (!in_array($nature,array('fixed','variable','mixed'),true)) throw new InvalidArgumentException('Naturaleza no válida.');
        if (!in_array($group,array('materials','labor','services','administration','sales','marketing','taxes','capex','other'),true)) throw new InvalidArgumentException('Grupo no válido.');
        if (!in_array($iva,array('recoverable','nonrecoverable','no_iva'),true)) throw new InvalidArgumentException('Tratamiento de IVA no válido.');
        if (!in_array($cash,array('operations','investment','financing'),true)) throw new InvalidArgumentException('Actividad de caja no válida.');
        $center = (int)($data['default_finance_center_id'] ?? 0);
        if ($center && !$this->validCenterId($center)) throw new InvalidArgumentException('El centro predeterminado no es válido.');
        if ($id > 0) {
            $stmt=$this->pdo->prepare("UPDATE cost_categories SET category_code=?,category_name=?,nature=?,category_group=?,default_iva_treatment=?,cash_activity=?,default_finance_center_id=?,is_active=?,description=?,updated_at=NOW() WHERE id=?");
            $stmt->execute(array($code,$name,$nature,$group,$iva,$cash,$center?:null,!empty($data['is_active'])?1:0,trim((string)($data['description']??'')),$id));
        } else {
            $stmt=$this->pdo->prepare("INSERT INTO cost_categories (category_code,category_name,nature,category_group,default_iva_treatment,cash_activity,default_finance_center_id,is_active,description,created_by) VALUES (?,?,?,?,?,?,?,?,?,?)");
            $stmt->execute(array($code,$name,$nature,$group,$iva,$cash,$center?:null,1,trim((string)($data['description']??'')),$userId));
            $id=(int)$this->pdo->lastInsertId();
        }
        $this->audit('cost_category',$id,'saved',null,null,$this->category($id),$userId);
        return $id;
    }

    public function saveRecurringRule(array $data, $userId)
    {
        $id=(int)($data['id']??0);
        $name=trim((string)($data['rule_name']??''));
        $description=trim((string)($data['description']??''));
        $category=$this->category((int)($data['category_id']??0));
        if ($name==='' || $description==='' || !$category || !(int)$category['is_active']) throw new InvalidArgumentException('Completa nombre, descripción y categoría activa de la recurrencia.');
        $iva=trim((string)($data['iva_treatment']??$category['default_iva_treatment']));
        if (!in_array($iva,array('recoverable','nonrecoverable','no_iva'),true)) throw new InvalidArgumentException('Tratamiento de IVA no válido.');
        $frequency=trim((string)($data['frequency']??'monthly'));
        if (!in_array($frequency,array('monthly','quarterly','semiannual','annual'),true)) throw new InvalidArgumentException('Frecuencia no válida.');
        $start=$this->normalizePeriod($data['start_period']??'');
        $end=trim((string)($data['end_period']??''));
        if ($end!=='') $end=$this->normalizePeriod($end);
        if ($end!=='' && $end<$start) throw new InvalidArgumentException('El período final no puede ser anterior al inicial.');
        $net=$this->money($data['net_amount']??0); $tax=$this->money($data['tax_amount']??0); $gross=$this->money($data['gross_amount']??($net+$tax)); if($net<0||$tax<0||$gross<0||$gross+0.02<$net) throw new InvalidArgumentException('Los montos de la recurrencia no son válidos.');
        $management=$iva==='recoverable'?$net:$gross;
        $supplierId=(int)($data['supplier_id']??0); $supplier=$supplierId?$this->supplier($supplierId):null; if($supplierId && !$supplier) throw new InvalidArgumentException('El proveedor seleccionado no es válido.');
        $center=(int)($data['finance_center_id']??0); if($center && !$this->validCenterId($center)) throw new InvalidArgumentException('El centro seleccionado no es válido.');
        if(!$center){$resolved=$this->centerAssignment->resolveCostCenter($supplierId,(int)$category['id'],0,'recurring');$center=(int)($resolved['center_id']??0);}
        $values=array($name,(int)$category['id'],$description,$supplierId?:null,$supplier?$supplier['display_name']:null,$center?:null,$iva,$net,$tax,$gross,$management,$frequency,max(1,min(28,(int)($data['day_of_month']??1))),$start,$end?:null,!empty($data['is_active'])?1:0,trim((string)($data['notes']??'')),$userId);
        if($id>0){
            $stmt=$this->pdo->prepare("UPDATE cost_recurring_rules SET rule_name=?,category_id=?,description=?,supplier_id=?,supplier_name=?,finance_center_id=?,iva_treatment=?,net_amount=?,tax_amount=?,gross_amount=?,management_cost_amount=?,frequency=?,day_of_month=?,start_period=?,end_period=?,is_active=?,notes=?,updated_by=?,updated_at=NOW() WHERE id=?");
            $values[]=$id; $stmt->execute($values);
        } else {
            $stmt=$this->pdo->prepare("INSERT INTO cost_recurring_rules (rule_name,category_id,description,supplier_id,supplier_name,finance_center_id,iva_treatment,net_amount,tax_amount,gross_amount,management_cost_amount,frequency,day_of_month,start_period,end_period,is_active,notes,created_by,updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
            $values[]=$userId; $stmt->execute($values); $id=(int)$this->pdo->lastInsertId();
        }
        $this->audit('cost_recurring_rule',$id,'saved',$start,null,array('rule_name'=>$name),$userId);
        return $id;
    }

    public function generateRecurring($period, $userId)
    {
        $period=$this->normalizePeriod($period); $this->assertPeriodOpen($period);
        $rules=$this->recurringRules(); $created=0; $skipped=0;
        $categoryStmt=$this->pdo->prepare('SELECT nature FROM cost_categories WHERE id=?');
        $insert=$this->pdo->prepare("INSERT IGNORE INTO cost_entries (period,entry_date,due_date,source_type,source_reference,category_id,nature,description,supplier_id,supplier_name,finance_center_id,iva_treatment,net_amount,tax_amount,gross_amount,management_cost_amount,status,recurring_rule_id,notes,approved_by,approved_at,created_by,updated_by) VALUES (?,?,?,'recurring',?,?,?,?,?,?,?,?,?,?,?,?,'approved',?,?,?,NOW(),?,?)");
        foreach($rules as $rule){
            if(!(int)$rule['is_active'] || $period<$rule['start_period'] || (!empty($rule['end_period']) && $period>$rule['end_period']) || !$this->ruleApplies($rule,$period)){ $skipped++; continue; }
            $categoryStmt->execute(array((int)$rule['category_id'])); $nature=$categoryStmt->fetchColumn()?:'fixed';
            $day=sprintf('%02d',max(1,min(28,(int)$rule['day_of_month']))); $date=$period.'-'.$day;
            $ruleCenter=(int)($rule['finance_center_id']??0); if(!$ruleCenter){$resolved=$this->centerAssignment->resolveCostCenter($rule['supplier_id'],$rule['category_id'],0,'recurring');$ruleCenter=(int)($resolved['center_id']??0);}
            $insert->execute(array($period,$date,$date,'recurring:'.$rule['id'].':'.$period,(int)$rule['category_id'],$nature,$rule['description'],$rule['supplier_id']?:null,$rule['supplier_name']?:null,$ruleCenter?:null,$rule['iva_treatment'],$rule['net_amount'],$rule['tax_amount'],$rule['gross_amount'],$rule['management_cost_amount'],(int)$rule['id'],$rule['notes'],$userId,$userId,$userId));
            if($insert->rowCount()>0){ $entryId=(int)$this->pdo->lastInsertId(); $created++; if($ruleCenter) $this->replaceAllocations($entryId,array(array('finance_center_id'=>$ruleCenter,'allocation_percent'=>100,'allocated_management_amount'=>(float)$rule['management_cost_amount']))); } else $skipped++;
        }
        $this->audit('cost_period',null,'recurring_generated',$period,null,array('created'=>$created,'skipped'=>$skipped),$userId);
        return array('created'=>$created,'skipped'=>$skipped);
    }

    public function savePayroll(array $data, $userId)
    {
        $id=(int)($data['id']??0); $period=$this->normalizePeriod($data['period']??''); $this->assertPeriodOpen($period);
        $code=strtoupper(trim((string)($data['person_code']??''))); $name=trim((string)($data['person_name']??''));
        if($code==='' || $name==='') throw new InvalidArgumentException('Ingresa código interno y nombre o agrupación.');
        $type=trim((string)($data['employment_type']??'employee')); if(!in_array($type,array('employee','fees','other'),true)) $type='other';
        $amountKeys=array('base_salary','allowances','bonuses','employer_charges','fees','other_costs'); $amounts=array(); $total=0;
        foreach($amountKeys as $key){ $amounts[$key]=$this->money($data[$key]??0); if($amounts[$key]<0) throw new InvalidArgumentException('Los componentes de remuneración no pueden ser negativos.'); $total+=$amounts[$key]; }
        $center=(int)($data['finance_center_id']??0); if($center && !$this->validCenterId($center)) throw new InvalidArgumentException('El centro seleccionado no es válido.');
        if(!$center){$remId=(int)$this->pdo->query("SELECT id FROM cost_categories WHERE category_code='REM' LIMIT 1")->fetchColumn();$resolved=$this->centerAssignment->resolveCostCenter(0,$remId,0,'manual');$center=(int)($resolved['center_id']??0);}
        $expected=$this->dateValue($data['expected_payment_date']??'',false,'Fecha prevista'); $effective=$this->dateValue($data['effective_payment_date']??'',false,'Fecha efectiva');
        $this->pdo->beginTransaction();
        try{
            if($id>0){
                $stmt=$this->pdo->prepare('SELECT * FROM cost_payroll_entries WHERE id=? FOR UPDATE'); $stmt->execute(array($id)); $previous=$stmt->fetch(PDO::FETCH_ASSOC); if(!$previous) throw new RuntimeException('Registro de remuneración no encontrado.'); if($previous['period']!==$period) $this->assertPeriodOpen($previous['period']);
                $stmt=$this->pdo->prepare("UPDATE cost_payroll_entries SET period=?,person_code=?,person_name=?,role_name=?,employment_type=?,base_salary=?,allowances=?,bonuses=?,employer_charges=?,fees=?,other_costs=?,total_company_cost=?,finance_center_id=?,expected_payment_date=?,effective_payment_date=?,notes=?,updated_by=?,updated_at=NOW() WHERE id=?");
                $stmt->execute(array($period,$code,$name,trim((string)($data['role_name']??'')),$type,$amounts['base_salary'],$amounts['allowances'],$amounts['bonuses'],$amounts['employer_charges'],$amounts['fees'],$amounts['other_costs'],$total,$center?:null,$expected,$effective,trim((string)($data['notes']??'')),$userId,$id));
            } else {
                $stmt=$this->pdo->prepare("INSERT INTO cost_payroll_entries (period,person_code,person_name,role_name,employment_type,base_salary,allowances,bonuses,employer_charges,fees,other_costs,total_company_cost,finance_center_id,expected_payment_date,effective_payment_date,notes,created_by,updated_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
                $stmt->execute(array($period,$code,$name,trim((string)($data['role_name']??'')),$type,$amounts['base_salary'],$amounts['allowances'],$amounts['bonuses'],$amounts['employer_charges'],$amounts['fees'],$amounts['other_costs'],$total,$center?:null,$expected,$effective,trim((string)($data['notes']??'')),$userId,$userId)); $id=(int)$this->pdo->lastInsertId();
            }
            $category=$this->pdo->query("SELECT id,nature FROM cost_categories WHERE category_code='REM' LIMIT 1")->fetch(PDO::FETCH_ASSOC); if(!$category) throw new RuntimeException('Falta la categoría REM de remuneraciones.');
            $status=$effective?'paid':'approved'; $entryDate=$period.'-'.sprintf('%02d',min(28,(int)date('d')));
            $stmt=$this->pdo->prepare("INSERT INTO cost_entries (period,entry_date,payment_date,source_type,source_reference,category_id,nature,description,finance_center_id,iva_treatment,net_amount,tax_amount,gross_amount,management_cost_amount,status,payroll_entry_id,approved_by,approved_at,created_by,updated_by) VALUES (?,?,?,'payroll',?,?,?,?,?,'no_iva',?,0,?,?,?, ?,?,NOW(),?,?) ON DUPLICATE KEY UPDATE period=VALUES(period),entry_date=VALUES(entry_date),payment_date=VALUES(payment_date),category_id=VALUES(category_id),nature=VALUES(nature),description=VALUES(description),finance_center_id=VALUES(finance_center_id),net_amount=VALUES(net_amount),gross_amount=VALUES(gross_amount),management_cost_amount=VALUES(management_cost_amount),status=VALUES(status),updated_by=VALUES(updated_by),updated_at=NOW()");
            $stmt->execute(array($period,$entryDate,$effective,'payroll:'.$id,(int)$category['id'],$category['nature'],'Costo empresa · '.$name,$center?:null,$total,$total,$total,$status,$id,$userId,$userId,$userId));
            $entryStmt=$this->pdo->prepare('SELECT id FROM cost_entries WHERE payroll_entry_id=?'); $entryStmt->execute(array($id)); $entryId=(int)$entryStmt->fetchColumn();
            $this->replaceAllocations($entryId,$center?array(array('finance_center_id'=>$center,'allocation_percent'=>100,'allocated_management_amount'=>$total)):array());
            $this->audit('cost_payroll',$id,'saved',$period,null,array('total_company_cost'=>$total),$userId);
            $this->pdo->commit(); return array('id'=>$id,'total_company_cost'=>$total);
        }catch(Throwable $e){ if($this->pdo->inTransaction())$this->pdo->rollBack(); throw $e; }
    }

    public function syncDteCosts($period, $userId)
    {
        $period=$this->normalizePeriod($period); $this->assertPeriodOpen($period);
        $other=$this->pdo->query("SELECT id,nature FROM cost_categories WHERE category_code='OTR' LIMIT 1")->fetch(PDO::FETCH_ASSOC); if(!$other) throw new RuntimeException('Falta la categoría OTR.');
        $stmt=$this->pdo->prepare("SELECT d.id AS inbox_local_id,d.inbox_document_id,d.document_date,d.payment_due_date,d.payment_paid_date,d.document_number,d.issuer_name,d.supplier_customer_id,d.finance_center_id,d.net_amount,d.taxes_amount,d.total_amount,d.payment_status,c.id AS collection_id,c.collection_status,c.due_date,c.last_payment_date FROM billing_collections c INNER JOIN facto_inbox_documents d ON d.environment=c.environment AND BINARY d.inbox_document_id=BINARY c.inbox_document_id WHERE c.environment='production' AND c.direction='payable' AND DATE_FORMAT(COALESCE(d.document_date,c.issue_date),'%Y-%m')=?");
        $stmt->execute(array($period)); $rows=$stmt->fetchAll(PDO::FETCH_ASSOC); $processed=0;
        $insert=$this->pdo->prepare("INSERT INTO cost_entries (period,entry_date,due_date,payment_date,source_type,source_reference,category_id,nature,description,supplier_id,supplier_name,finance_center_id,iva_treatment,net_amount,tax_amount,gross_amount,management_cost_amount,status,linked_inbox_document_id,linked_collection_id,approved_by,approved_at,created_by,updated_by) VALUES (?,?,?,?,'dte',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, ?,NOW(),?,?) ON DUPLICATE KEY UPDATE entry_date=VALUES(entry_date),due_date=VALUES(due_date),payment_date=VALUES(payment_date),supplier_id=VALUES(supplier_id),supplier_name=VALUES(supplier_name),finance_center_id=COALESCE(finance_center_id,VALUES(finance_center_id)),net_amount=VALUES(net_amount),tax_amount=VALUES(tax_amount),gross_amount=VALUES(gross_amount),management_cost_amount=CASE WHEN iva_treatment='recoverable' THEN VALUES(net_amount) ELSE VALUES(gross_amount) END,status=VALUES(status),linked_collection_id=VALUES(linked_collection_id),updated_by=VALUES(updated_by),updated_at=NOW()");
        foreach($rows as $row){
            $net=(float)$row['net_amount']; $tax=(float)$row['taxes_amount']; $gross=(float)$row['total_amount']; if($net<=0 && $tax<=0)$net=$gross; if($tax<=0 && $gross>$net)$tax=round($gross-$net,2);
            $iva=$tax>0?'recoverable':'no_iva'; $management=$iva==='recoverable'?$net:$gross; $status=in_array($row['collection_status'],array('anulado','rechazado'),true)?'void':($row['collection_status']==='pagado'?'paid':'approved');
            $date=$row['document_date']?:$period.'-01'; $due=$row['due_date']?:$row['payment_due_date']; $paid=$row['last_payment_date']?:$row['payment_paid_date'];
            $documentCenter=(int)($row['finance_center_id']??0); $resolved=$this->centerAssignment->resolveCostCenter($row['supplier_customer_id'],(int)$other['id'],$documentCenter,'dte'); $resolvedCenter=(int)($resolved['center_id']??0); if($resolvedCenter){if($resolvedCenter!==$documentCenter){$this->pdo->prepare('UPDATE facto_inbox_documents SET finance_center_id=?,updated_at=NOW() WHERE id=?')->execute(array($resolvedCenter,(int)$row['inbox_local_id']));$this->pdo->prepare('UPDATE billing_collections SET finance_center_id=?,updated_at=NOW() WHERE id=?')->execute(array($resolvedCenter,(int)$row['collection_id']));}$documentCenter=$resolvedCenter;}
            $insert->execute(array($period,$date,$due,$paid,'dte:production:'.$row['inbox_document_id'],(int)$other['id'],$other['nature'],'DTE '.$row['document_number'].' · '.$row['issuer_name'],$row['supplier_customer_id']?:null,$row['issuer_name'],$documentCenter?:null,$iva,$net,$tax,$gross,$management,$status,(int)$row['inbox_local_id'],(int)$row['collection_id'],$userId,$userId,$userId));
            $find=$this->pdo->prepare('SELECT id,finance_center_id,management_cost_amount FROM cost_entries WHERE linked_inbox_document_id=?'); $find->execute(array((int)$row['inbox_local_id'])); $entry=$find->fetch(PDO::FETCH_ASSOC);
            if($entry && $entry['finance_center_id']){
                $has=$this->pdo->prepare('SELECT COUNT(*) FROM cost_allocations WHERE cost_entry_id=?'); $has->execute(array((int)$entry['id'])); if(!(int)$has->fetchColumn()) $this->replaceAllocations((int)$entry['id'],array(array('finance_center_id'=>(int)$entry['finance_center_id'],'allocation_percent'=>100,'allocated_management_amount'=>(float)$entry['management_cost_amount'])));
            }
            $processed++;
        }
        $this->audit('cost_period',null,'dte_synced',$period,null,array('processed'=>$processed),$userId);
        return array('processed'=>$processed,'available'=>count($rows));
    }

    public function setPeriodClosed($period, $closed, $notes, $userId)
    {
        $period=$this->normalizePeriod($period);
        if($closed){
            $draft=$this->pdo->prepare("SELECT COUNT(*) FROM cost_entries WHERE period=? AND status='draft'"); $draft->execute(array($period)); if((int)$draft->fetchColumn()>0) throw new RuntimeException('Aprueba o anula todos los borradores antes de cerrar el período.');
            $unclassified=$this->pdo->prepare("SELECT COUNT(*) FROM cost_entries e LEFT JOIN cost_categories c ON c.id=e.category_id WHERE e.period=? AND e.status<>'void' AND (e.category_id IS NULL OR c.category_code='OTR')"); $unclassified->execute(array($period)); if((int)$unclassified->fetchColumn()>0) throw new RuntimeException('Clasifica los costos marcados como Otros antes de cerrar el período.');
            $withoutCenter=$this->pdo->prepare("SELECT COUNT(*) FROM cost_entries e WHERE e.period=? AND e.status<>'void' AND e.finance_center_id IS NULL AND NOT EXISTS (SELECT 1 FROM cost_allocations a WHERE a.cost_entry_id=e.id)"); $withoutCenter->execute(array($period)); if((int)$withoutCenter->fetchColumn()>0) throw new RuntimeException('Asigna un centro de costo a todos los movimientos antes de cerrar el período.');
            $summary=$this->summary($period); $snapshot=array('summary'=>$summary,'by_category'=>$this->groupSummary($period,'category'),'by_nature'=>$this->groupSummary($period,'nature'),'by_center'=>$this->groupSummary($period,'center'));
            $stmt=$this->pdo->prepare("INSERT INTO cost_monthly_closes (period,close_status,management_cost_total,net_total,tax_total,gross_total,entries_count,snapshot_json,notes,closed_by,closed_at) VALUES (?,'closed',?,?,?,?,?,?,?, ?,NOW()) ON DUPLICATE KEY UPDATE close_status='closed',management_cost_total=VALUES(management_cost_total),net_total=VALUES(net_total),tax_total=VALUES(tax_total),gross_total=VALUES(gross_total),entries_count=VALUES(entries_count),snapshot_json=VALUES(snapshot_json),notes=VALUES(notes),closed_by=VALUES(closed_by),closed_at=NOW(),reopened_by=NULL,reopened_at=NULL");
            $stmt->execute(array($period,$summary['management_total'],$summary['net_total'],$summary['tax_total'],$summary['gross_total'],$summary['entries_count'],json_encode($snapshot,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),trim((string)$notes),$userId));
            $action='closed';
        }else{
            $stmt=$this->pdo->prepare("UPDATE cost_monthly_closes SET close_status='open',notes=?,reopened_by=?,reopened_at=NOW(),updated_at=NOW() WHERE period=?"); $stmt->execute(array(trim((string)$notes),$userId,$period)); $action='reopened';
        }
        $this->audit('cost_period',null,$action,$period,null,array('notes'=>$notes),$userId);
    }

    private function assertInstalled(){
        $stmt=$this->pdo->query("SHOW TABLES LIKE 'cost_entries'");
        if(!$stmt->fetchColumn()) throw new RuntimeException('El módulo de costos aún no está instalado. Ejecuta la migración v8.8 desde phpMyAdmin.');
    }
    private function assertPeriodOpen($period){ $close=$this->periodClose($period); if($close && $close['close_status']==='closed') throw new RuntimeException('El período '.$period.' está cerrado. Reábrelo antes de modificar información.'); }
    private function periodClose($period){$s=$this->pdo->prepare('SELECT * FROM cost_monthly_closes WHERE period=? LIMIT 1');$s->execute(array($period));return $s->fetch(PDO::FETCH_ASSOC)?:null;}
    private function entryForUpdate($id){$s=$this->pdo->prepare('SELECT * FROM cost_entries WHERE id=? LIMIT 1');$s->execute(array($id));return $s->fetch(PDO::FETCH_ASSOC)?:null;}
    private function category($id){$s=$this->pdo->prepare('SELECT * FROM cost_categories WHERE id=? LIMIT 1');$s->execute(array($id));return $s->fetch(PDO::FETCH_ASSOC)?:null;}
    private function supplier($id){$s=$this->pdo->prepare("SELECT id,COALESCE(NULLIF(commercial_name,''),NULLIF(customer_name,''),legal_name) AS display_name FROM crm_customers WHERE id=? AND is_supplier=1 AND deleted_at IS NULL LIMIT 1");$s->execute(array($id));return $s->fetch(PDO::FETCH_ASSOC)?:null;}
    private function validCenterId($id){$s=$this->pdo->prepare("SELECT COUNT(*) FROM finance_centers WHERE id=? AND is_active=1 AND center_type IN ('cost','both')");$s->execute(array((int)$id));return (int)$s->fetchColumn()>0;}
    private function categories($activeOnly){$sql='SELECT c.*,fc.center_name AS default_center_name FROM cost_categories c LEFT JOIN finance_centers fc ON fc.id=c.default_finance_center_id'.($activeOnly?' WHERE c.is_active=1':'').' ORDER BY c.is_active DESC,c.category_group,c.category_name';return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);}
    private function centers(){return $this->pdo->query("SELECT id,center_code,center_name,center_type FROM finance_centers WHERE is_active=1 AND center_type IN ('cost','both') ORDER BY is_default DESC,center_name")->fetchAll(PDO::FETCH_ASSOC);}
    private function suppliers(){return $this->pdo->query("SELECT id,COALESCE(NULLIF(commercial_name,''),NULLIF(customer_name,''),legal_name) AS display_name,customer_tax_id,COALESCE(supplier_default_cost_center_id,default_cost_center_id) AS supplier_default_cost_center_id FROM crm_customers WHERE is_supplier=1 AND deleted_at IS NULL AND COALESCE(supplier_status,'active')='active' ORDER BY display_name LIMIT 1000")->fetchAll(PDO::FETCH_ASSOC);}
    private function recurringRules(){return $this->pdo->query("SELECT r.*,c.category_code,c.category_name,fc.center_name FROM cost_recurring_rules r JOIN cost_categories c ON c.id=r.category_id LEFT JOIN finance_centers fc ON fc.id=r.finance_center_id ORDER BY r.is_active DESC,r.rule_name")->fetchAll(PDO::FETCH_ASSOC);}
    private function payroll($period){$s=$this->pdo->prepare("SELECT p.*,fc.center_name FROM cost_payroll_entries p LEFT JOIN finance_centers fc ON fc.id=p.finance_center_id WHERE p.period=? ORDER BY p.person_name");$s->execute(array($period));return $s->fetchAll(PDO::FETCH_ASSOC);}
    private function entries($period,$includePayroll){$sql="SELECT e.*,c.category_code,c.category_name,c.category_group,c.cash_activity,fc.center_name,bc.document_number AS linked_document_number,bc.collection_status,bc.paid_amount AS linked_paid_amount,bc.balance_amount AS linked_balance_amount,COALESCE(bp.payment_count,0) AS payment_count,bp.last_payment_date AS linked_last_payment_date FROM cost_entries e LEFT JOIN cost_categories c ON c.id=e.category_id LEFT JOIN finance_centers fc ON fc.id=e.finance_center_id LEFT JOIN billing_collections bc ON bc.id=e.linked_collection_id LEFT JOIN (SELECT collection_id,COUNT(*) AS payment_count,MAX(payment_date) AS last_payment_date FROM billing_payments WHERE environment='production' AND direction='paid' AND COALESCE(is_voided,0)=0 GROUP BY collection_id) bp ON bp.collection_id=e.linked_collection_id WHERE e.period=?".($includePayroll?'':" AND e.source_type<>'payroll'")." ORDER BY e.entry_date DESC,e.id DESC LIMIT 1000";$s=$this->pdo->prepare($sql);$s->execute(array($period));return $s->fetchAll(PDO::FETCH_ASSOC);}
    private function allocationsByEntry(array $ids){if(!$ids)return array();$place=implode(',',array_fill(0,count($ids),'?'));$s=$this->pdo->prepare("SELECT a.*,fc.center_name FROM cost_allocations a JOIN finance_centers fc ON fc.id=a.finance_center_id WHERE a.cost_entry_id IN ($place) ORDER BY a.id");$s->execute($ids);$out=array();foreach($s->fetchAll(PDO::FETCH_ASSOC) as $row)$out[(int)$row['cost_entry_id']][]=$row;return $out;}
    private function summary($period){$s=$this->pdo->prepare("SELECT COUNT(*) AS entries_count,COALESCE(SUM(management_cost_amount),0) AS management_total,COALESCE(SUM(net_amount),0) AS net_total,COALESCE(SUM(tax_amount),0) AS tax_total,COALESCE(SUM(gross_amount),0) AS gross_total,COALESCE(SUM(CASE WHEN nature='fixed' THEN management_cost_amount ELSE 0 END),0) AS fixed_total,COALESCE(SUM(CASE WHEN nature='variable' THEN management_cost_amount ELSE 0 END),0) AS variable_total,COALESCE(SUM(CASE WHEN nature='mixed' THEN management_cost_amount ELSE 0 END),0) AS mixed_total,COALESCE(SUM(CASE WHEN source_type='payroll' THEN management_cost_amount ELSE 0 END),0) AS payroll_total,COALESCE(SUM(CASE WHEN status='draft' THEN 1 ELSE 0 END),0) AS draft_count FROM cost_entries WHERE period=? AND status<>'void'");$s->execute(array($period));return $s->fetch(PDO::FETCH_ASSOC);}
    private function groupSummary($period,$mode){if($mode==='nature'){$sql="SELECT nature AS item_key,nature AS label,SUM(management_cost_amount) AS amount,COUNT(*) AS entries_count FROM cost_entries WHERE period=? AND status<>'void' GROUP BY nature ORDER BY amount DESC";}elseif($mode==='center'){$sql="SELECT COALESCE(CAST(a.finance_center_id AS CHAR),'none') AS item_key,COALESCE(fc.center_name,'Sin centro') AS label,SUM(COALESCE(a.allocated_management_amount,e.management_cost_amount)) AS amount,COUNT(DISTINCT e.id) AS entries_count FROM cost_entries e LEFT JOIN cost_allocations a ON a.cost_entry_id=e.id LEFT JOIN finance_centers fc ON fc.id=a.finance_center_id WHERE e.period=? AND e.status<>'void' GROUP BY item_key,label ORDER BY amount DESC";}else{$sql="SELECT COALESCE(CAST(e.category_id AS CHAR),'none') AS item_key,COALESCE(c.category_name,'Sin categoría') AS label,SUM(e.management_cost_amount) AS amount,COUNT(*) AS entries_count FROM cost_entries e LEFT JOIN cost_categories c ON c.id=e.category_id WHERE e.period=? AND e.status<>'void' GROUP BY item_key,label ORDER BY amount DESC";}$s=$this->pdo->prepare($sql);$s->execute(array($period));return $s->fetchAll(PDO::FETCH_ASSOC);}
    private function availableDteCount($period){$s=$this->pdo->prepare("SELECT COUNT(*) FROM billing_collections c JOIN facto_inbox_documents d ON d.environment=c.environment AND BINARY d.inbox_document_id=BINARY c.inbox_document_id LEFT JOIN cost_entries e ON e.linked_inbox_document_id=d.id WHERE c.environment='production' AND c.direction='payable' AND DATE_FORMAT(COALESCE(d.document_date,c.issue_date),'%Y-%m')=? AND e.id IS NULL");$s->execute(array($period));return (int)$s->fetchColumn();}
    private function normalizeAllocations($raw,$primaryCenter,$management){if(is_string($raw)){ $decoded=json_decode($raw,true); $raw=is_array($decoded)?$decoded:array(); } if(!is_array($raw))$raw=array(); if(!$raw && $primaryCenter)$raw=array(array('finance_center_id'=>$primaryCenter,'allocation_percent'=>100));$out=array();$seen=array();$sum=0;foreach($raw as $item){$center=(int)($item['finance_center_id']??0);$pct=round((float)($item['allocation_percent']??0),4);if($center<=0||$pct<=0)continue;if(!$this->validCenterId($center))throw new InvalidArgumentException('Una asignación contiene un centro no válido.');if(isset($seen[$center]))throw new InvalidArgumentException('No repitas el mismo centro en las asignaciones.');$seen[$center]=true;if($pct>100)throw new InvalidArgumentException('Una asignación no puede superar 100%.');$sum+=$pct;$out[]=array('finance_center_id'=>$center,'allocation_percent'=>$pct,'allocated_management_amount'=>round($management*$pct/100,2));}if($out && abs($sum-100)>0.01)throw new InvalidArgumentException('Las asignaciones por centro deben sumar 100%.');return $out;}
    private function replaceAllocations($entryId,array $allocations){$this->pdo->prepare('DELETE FROM cost_allocations WHERE cost_entry_id=?')->execute(array($entryId));if(!$allocations)return;$s=$this->pdo->prepare('INSERT INTO cost_allocations (cost_entry_id,finance_center_id,allocation_percent,allocated_management_amount) VALUES (?,?,?,?)');foreach($allocations as $a)$s->execute(array($entryId,$a['finance_center_id'],$a['allocation_percent'],$a['allocated_management_amount']));}
    private function dateValue($value,$required,$label){$value=trim((string)$value);if($value===''&&!$required)return null;if(!preg_match('/^\d{4}-\d{2}-\d{2}$/',$value)||!strtotime($value))throw new InvalidArgumentException($label.' no válida.');return $value;}
    private function money($value){if(is_string($value)){$value=str_replace(array('$',' '),array('',''),trim($value));if(strpos($value,',')!==false&&strpos($value,'.')!==false)$value=str_replace(',','.',str_replace('.','',$value));elseif(strpos($value,',')!==false)$value=str_replace(',','.',$value);}return round((float)$value,2);}
    private function ruleApplies($rule,$period){$start=new DateTime($rule['start_period'].'-01');$current=new DateTime($period.'-01');$diff=((int)$current->format('Y')-(int)$start->format('Y'))*12+((int)$current->format('m')-(int)$start->format('m'));$step=array('monthly'=>1,'quarterly'=>3,'semiannual'=>6,'annual'=>12)[$rule['frequency']]??1;return $diff>=0&&$diff%$step===0;}
    private function audit($entityType,$entityId,$action,$period,$before,$after,$userId){$s=$this->pdo->prepare('INSERT INTO cost_audit_log (entity_type,entity_id,action_key,period,before_json,after_json,actor_user_id) VALUES (?,?,?,?,?,?,?)');$s->execute(array($entityType,$entityId,$action,$period,$before===null?null:json_encode($before,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),$after===null?null:json_encode($after,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),$userId));}
}
