<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Database;
use PDO;
use PDOException;
use RuntimeException;

final class DocumentManager
{
    private PDO $db;
    private TarifClientService $tarifClientService;

    public function __construct(?TarifClientService $tarifClientService = null)
    {
        $this->db = Database::connection();
        $this->tarifClientService = $tarifClientService ?? new TarifClientService();
    }

    public function allowedTypes(): array
    {
        return ['DEVIS', 'BL', 'FACTURE', 'AVOIR'];
    }

    public function typeLabel(string $type): string
    {
        return match (strtoupper($type)) {
            'DEVIS' => 'Devis',
            'BL' => 'Bon de livraison',
            'FACTURE' => 'Facture',
            'AVOIR' => 'Avoir',
            default => strtoupper($type),
        };
    }

    public function nextNumber(string $type): string
    {
        $type = strtoupper($type);
        $year = date('Y');
        $prefix = match ($type) {
            'DEVIS' => 'DV' . $year . '-',
            'BL' => 'BL' . $year . '-',
            'FACTURE' => 'FA' . $year . '-',
            'AVOIR' => 'AV' . $year . '-',
            default => $type . $year . '-',
        };

        $stmt = $this->db->prepare(
            'SELECT COALESCE(MAX(CAST(RIGHT(numero, 6) AS UNSIGNED)), 0)
             FROM documents
             WHERE type_document = :type
               AND numero LIKE :prefix'
        );
        $stmt->execute([
            'type' => $type,
            'prefix' => $prefix . '%',
        ]);

        $number = (int)$stmt->fetchColumn() + 1;
        return $prefix . str_pad((string)$number, 6, '0', STR_PAD_LEFT);
    }

	public function create(array $data): int
	{
		$type = strtoupper(trim((string)($data['type_document'] ?? '')));

		if (!in_array($type, $this->allowedTypes(), true)) {
			throw new RuntimeException('Type de document invalide.');
		}

		$clientId = (int)($data['client_id'] ?? 0);
		if ($clientId <= 0) {
			throw new RuntimeException('Client obligatoire.');
		}

		$lines = $this->normalizeLines($data['lignes'] ?? [], $clientId);
		if (empty($lines)) {
			throw new RuntimeException('Veuillez ajouter au moins une ligne au document.');
		}

		$totals = $this->calculateTotals($lines);
		$requestedNumber = trim((string)($data['numero'] ?? ''));
		$automaticNumber = $requestedNumber === '';

		$dateDocument = !empty($data['date_document'])
			? (string)$data['date_document']
			: date('Y-m-d');

		$dateEcheance = !empty($data['date_echeance'])
			? (string)$data['date_echeance']
			: null;

		$parentDocumentId = !empty($data['parent_document_id'])
			? (int)$data['parent_document_id']
			: null;

		if ($parentDocumentId !== null && $parentDocumentId <= 0) {
			$parentDocumentId = null;
		}

		$statut = $this->defaultStatus($type);
		$startedHere = false;
		$numberLock = null;

		if (!$this->db->inTransaction()) {
			$this->db->beginTransaction();
			$startedHere = true;
		}

		try {
			if ($automaticNumber) {
				$numberLock = $this->acquireNumberLock($type);
			}

			$stmt = $this->db->prepare("
				INSERT INTO documents (
					type_document, parent_document_id, client_id, numero, date_document, date_echeance, statut,
					total_ht, total_tva, total_ttc, montant_regle, reste_du, marge_ht, marge_pourcentage, observations
				) VALUES (
					:type_document, :parent_document_id, :client_id, :numero, :date_document, :date_echeance, :statut,
					:total_ht, :total_tva, :total_ttc, :montant_regle, :reste_du, :marge_ht, :marge_pourcentage, :observations
				)
			");

			$documentId = 0;
			$maxAttempts = $automaticNumber ? 3 : 1;

			for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
				$numero = $automaticNumber ? $this->nextNumber($type) : $requestedNumber;

				try {
					$stmt->execute([
						'type_document' => $type,
						'parent_document_id' => $parentDocumentId,
						'client_id' => $clientId,
						'numero' => $numero,
						'date_document' => $dateDocument,
						'date_echeance' => $dateEcheance,
						'statut' => $statut,
						'total_ht' => $totals['total_ht'],
						'total_tva' => $totals['total_tva'],
						'total_ttc' => $totals['total_ttc'],
						'montant_regle' => 0,
						'reste_du' => $type === 'FACTURE' ? $totals['total_ttc'] : 0,
						'marge_ht' => $totals['marge_ht'],
						'marge_pourcentage' => $totals['marge_pourcentage'],
						'observations' => trim((string)($data['observations'] ?? '')),
					]);
					$documentId = (int)$this->db->lastInsertId();
					break;
				} catch (PDOException $exception) {
					if (!$automaticNumber || !$this->isDuplicateNumberException($exception) || $attempt === $maxAttempts) {
						throw $exception;
					}
				}
			}

			if ($documentId <= 0) {
				throw new RuntimeException('Impossible de générer un numéro de document unique.');
			}

			$this->insertLines($documentId, $lines);

			if ($type === 'FACTURE') {
				$this->syncLegacyFacture($documentId);
			}

			if ($this->shouldDecrementStock($type, $parentDocumentId)) {
				$this->decrementStockForDocument($documentId, $type);
			}

			if ($startedHere && $this->db->inTransaction()) {
				$this->db->commit();
			}

			return $documentId;
		} catch (\Throwable $exception) {
			if ($startedHere && $this->db->inTransaction()) {
				$this->db->rollBack();
			}
			throw $exception;
		} finally {
			if ($numberLock !== null) {
				$this->releaseNumberLock($numberLock);
			}
		}
	}

	private function acquireNumberLock(string $type): string
	{
		$lockName = 'darlink_document_number_' . strtoupper($type) . '_' . date('Y');
		$stmt = $this->db->prepare('SELECT GET_LOCK(:lock_name, 10)');
		$stmt->execute(['lock_name' => $lockName]);

		if ((int)$stmt->fetchColumn() !== 1) {
			throw new RuntimeException('La numérotation est momentanément occupée. Veuillez réessayer.');
		}

		return $lockName;
	}

	private function releaseNumberLock(string $lockName): void
	{
		try {
			$stmt = $this->db->prepare('SELECT RELEASE_LOCK(:lock_name)');
			$stmt->execute(['lock_name' => $lockName]);
		} catch (\Throwable) {
			// La connexion libère également le verrou à sa fermeture.
		}
	}

	private function isDuplicateNumberException(PDOException $exception): bool
	{
		$sqlState = (string)$exception->getCode();
		$driverCode = isset($exception->errorInfo[1]) ? (int)$exception->errorInfo[1] : 0;
		return $sqlState === '23000' && $driverCode === 1062;
	}

    public function transform(int $sourceDocumentId, string $targetType): int
    {
        $targetType = strtoupper($targetType);
        $source = $this->findDocument($sourceDocumentId);
        if (!$source) {
            throw new RuntimeException('Document source introuvable.');
        }
        if (!in_array($targetType, $this->allowedTypes(), true)) {
            throw new RuntimeException('Type de document cible invalide.');
        }
        if (!$this->canTransform((string)$source['type_document'], $targetType)) {
            throw new RuntimeException('Transformation non autorisée.');
        }

        $existing = $this->findChildByType($sourceDocumentId, $targetType);
        if ($existing) {
            return (int)$existing['id'];
        }

        $lines = $this->lines($sourceDocumentId);
        $data = [
            'type_document' => $targetType,
            'parent_document_id' => $sourceDocumentId,
            'client_id' => (int)$source['client_id'],
            'date_document' => date('Y-m-d'),
            'date_echeance' => $targetType === 'FACTURE' ? date('Y-m-d', strtotime('+30 days')) : null,
            'observations' => trim((string)($source['observations'] ?? '')),
            'lignes' => $lines,
        ];

        $newId = $this->create($data);
        $this->markSourceTransformed($sourceDocumentId, $targetType);
        return $newId;
    }

    public function createAvoirFromFacture(int $factureId, string $avoirType, string $motif, array $requestedLines, string $observations = ''): int
    {
        $facture = $this->findDocument($factureId);
        if (!$facture || strtoupper((string)$facture['type_document']) !== 'FACTURE') {
            throw new RuntimeException('Facture source introuvable.');
        }

        $avoirType = strtoupper(trim($avoirType));
        if (!in_array($avoirType, ['COMMERCIAL', 'RETOUR'], true)) {
            throw new RuntimeException('Type d\'avoir invalide.');
        }

        $sourceLines = $this->lines($factureId);
        $linesById = [];
        foreach ($sourceLines as $line) {
            $linesById[(int)$line['id']] = $line;
        }

        $avoirLines = [];
        foreach ($requestedLines as $lineId => $request) {
            $lineId = (int)$lineId;
            if (!isset($linesById[$lineId])) {
                continue;
            }

            $sourceLine = $linesById[$lineId];
            $maxQuantity = (float)($sourceLine['quantite'] ?? 0);
            $quantity = $this->number($request['quantite'] ?? 0);
            if ($quantity <= 0) {
                continue;
            }
            if ($quantity > $maxQuantity) {
                $quantity = $maxQuantity;
            }

            $sourceLine['quantite'] = $quantity;
            $avoirLines[] = $sourceLine;
        }

        if (empty($avoirLines)) {
            throw new RuntimeException('Veuillez sélectionner au moins une ligne à reprendre.');
        }

        $motif = trim($motif) ?: ($avoirType === 'RETOUR' ? 'Retour marchandises' : 'Avoir commercial');
        $fullObservation = trim("Type : {$avoirType} | Motif : {$motif}" . ($observations !== '' ? "\n" . $observations : ''));

$startedHere = false;

if (!$this->db->inTransaction()) {
    $this->db->beginTransaction();
    $startedHere = true;
}

try {
    $avoirId = $this->create([
        'type_document' => 'AVOIR',
        'parent_document_id' => $factureId,
        'client_id' => (int)$facture['client_id'],
        'date_document' => date('Y-m-d'),
        'date_echeance' => null,
        'observations' => $fullObservation,
        'lignes' => $avoirLines,
    ]);

    if ($avoirType === 'RETOUR') {
        $this->returnStockForAvoir($avoirId);
    }

    if ($startedHere && $this->db->inTransaction()) {
        $this->db->commit();
    }

    return $avoirId;
} catch (\Throwable $exception) {
    if ($startedHere && $this->db->inTransaction()) {
        $this->db->rollBack();
    }

    throw $exception;
}
    }


    public function prepareForEdit(int $documentId): array
    {
        $document = $this->findDocument($documentId);
        if (!$document) {
            throw new RuntimeException('Document introuvable.');
        }
        if (strtoupper((string)$document['type_document']) !== 'FACTURE') {
            throw new RuntimeException('Seules les factures peuvent être modifiées dans cette version.');
        }
        $totalTtc = (float)($document['total_ttc'] ?? 0);
        $montantRegle = (float)($document['montant_regle'] ?? 0);
        $resteDu = (float)($document['reste_du'] ?? max(0, $totalTtc - $montantRegle));
        if ($resteDu <= 0.005 || ($totalTtc > 0 && $montantRegle >= $totalTtc - 0.005)) {
            throw new RuntimeException('Cette facture est réglée. Elle ne peut plus être modifiée.');
        }

        return [
            'document' => $document,
            'lignes' => $this->lines($documentId),
        ];
    }

    /**
     * Retourne le document commun associé à une facture historique et le crée
     * à la volée lorsqu'une facture issue de l'ancien circuit n'a pas été
     * synchronisée dans `documents` par la migration initiale.
     */
    public function ensureLegacyInvoiceDocument(int $factureId): int
    {
        if ($factureId <= 0 || !$this->tableExists('factures') || !$this->tableExists('factures_lignes')) {
            throw new RuntimeException('Facture introuvable.');
        }

        $existing = $this->db->prepare("SELECT id FROM documents WHERE actif = 1 AND type_document = 'FACTURE' AND ((source_table = 'factures' AND source_id = :id) OR numero = (SELECT numero FROM factures WHERE id = :id2)) ORDER BY (source_table = 'factures' AND source_id = :id3) DESC, id DESC LIMIT 1");
        $existing->execute(['id' => $factureId, 'id2' => $factureId, 'id3' => $factureId]);
        $documentId = (int)($existing->fetchColumn() ?: 0);
        if ($documentId > 0) {
            $this->db->prepare("UPDATE documents SET source_table = 'factures', source_id = :facture_id WHERE id = :id")->execute(['facture_id' => $factureId, 'id' => $documentId]);
            return $documentId;
        }

        $stmt = $this->db->prepare('SELECT * FROM factures WHERE id = :id AND actif = 1 LIMIT 1');
        $stmt->execute(['id' => $factureId]);
        $facture = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$facture) {
            throw new RuntimeException('Facture introuvable.');
        }
        if ((float)($facture['reste_du'] ?? 0) <= 0.005) {
            throw new RuntimeException('Cette facture est réglée. Elle ne peut plus être modifiée.');
        }

        $parentDocumentId = null;
        $bonLivraisonId = (int)($facture['bon_livraison_id'] ?? 0);
        if ($bonLivraisonId > 0) {
            $parent = $this->db->prepare("SELECT id FROM documents WHERE actif = 1 AND type_document = 'BL' AND source_table = 'bons_livraison' AND source_id = :id LIMIT 1");
            $parent->execute(['id' => $bonLivraisonId]);
            $parentDocumentId = $parent->fetchColumn() ?: null;
        }

        $this->db->beginTransaction();
        try {
            $insert = $this->db->prepare("INSERT INTO documents (type_document, parent_document_id, source_table, source_id, client_id, numero, date_document, date_echeance, statut, total_ht, total_tva, total_ttc, montant_regle, reste_du, marge_ht, marge_pourcentage, observations, actif) VALUES ('FACTURE', :parent_document_id, 'factures', :source_id, :client_id, :numero, :date_document, :date_echeance, :statut, :total_ht, :total_tva, :total_ttc, :montant_regle, :reste_du, :marge_ht, :marge_pourcentage, :observations, 1)");
            $insert->execute([
                'parent_document_id' => $parentDocumentId,
                'source_id' => $factureId,
                'client_id' => (int)$facture['client_id'],
                'numero' => (string)$facture['numero'],
                'date_document' => (string)$facture['date_facture'],
                'date_echeance' => $facture['date_echeance'] ?: null,
                'statut' => (string)($facture['statut'] ?? 'émise'),
                'total_ht' => (float)($facture['total_ht'] ?? 0),
                'total_tva' => (float)($facture['total_tva'] ?? 0),
                'total_ttc' => (float)($facture['total_ttc'] ?? 0),
                'montant_regle' => (float)($facture['montant_regle'] ?? 0),
                'reste_du' => (float)($facture['reste_du'] ?? 0),
                'marge_ht' => (float)($facture['marge_ht'] ?? 0),
                'marge_pourcentage' => (float)($facture['marge_pourcentage'] ?? 0),
                'observations' => (string)($facture['observations'] ?? ''),
            ]);
            $documentId = (int)$this->db->lastInsertId();

            $copy = $this->db->prepare("INSERT INTO documents_lignes (document_id, source_table, source_id, article_id, reference, designation, unite, quantite, prix_achat_ht, prix_vente_ht, remise, taux_tva, total_ht, total_tva, total_ttc, marge_ht, marge_pourcentage, ordre) SELECT :document_id, 'factures_lignes', id, article_id, reference, designation, unite, quantite, prix_achat_ht, prix_vente_ht, remise, taux_tva, total_ht, total_tva, total_ttc, marge_ht, marge_pourcentage, ordre FROM factures_lignes WHERE facture_id = :facture_id ORDER BY ordre, id");
            $copy->execute(['document_id' => $documentId, 'facture_id' => $factureId]);
            if ($copy->rowCount() === 0) {
                throw new RuntimeException('Cette facture ne contient aucune ligne modifiable.');
            }
            $this->db->commit();
            return $documentId;
        } catch (\Throwable $exception) {
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            }
            throw $exception;
        }
    }

    public function updateInvoice(int $documentId, array $data): void
    {
        $current = $this->findDocument($documentId);
        if (!$current) {
            throw new RuntimeException('Facture introuvable.');
        }
        if (strtoupper((string)$current['type_document']) !== 'FACTURE') {
            throw new RuntimeException('Seules les factures peuvent être modifiées dans cette version.');
        }
        $currentTotalTtc = (float)($current['total_ttc'] ?? 0);
        $currentMontantRegle = (float)($current['montant_regle'] ?? 0);
        $currentResteDu = (float)($current['reste_du'] ?? max(0, $currentTotalTtc - $currentMontantRegle));
        if ($currentResteDu <= 0.005 || ($currentTotalTtc > 0 && $currentMontantRegle >= $currentTotalTtc - 0.005)) {
            throw new RuntimeException('Cette facture est réglée. Elle ne peut plus être modifiée.');
        }

        $clientId = (int)($data['client_id'] ?? $current['client_id']);
        if ($clientId <= 0) {
            throw new RuntimeException('Client obligatoire.');
        }

        $oldLines = $this->lines($documentId);
        $newLines = $this->normalizeLines($data['lignes'] ?? [], $clientId);
        if (empty($newLines)) {
            throw new RuntimeException('Veuillez conserver au moins une ligne sur la facture.');
        }

        $totals = $this->calculateTotals($newLines);
        $dateDocument = $data['date_document'] ?: ($current['date_document'] ?? date('Y-m-d'));
        $dateEcheance = $data['date_echeance'] ?: null;
        $montantRegle = (float)($current['montant_regle'] ?? 0);
        if ($totals['total_ttc'] + 0.005 < $montantRegle) {
            throw new RuntimeException('Le nouveau total TTC ne peut pas être inférieur au montant déjà encaissé.');
        }
        $resteDu = max(0, round($totals['total_ttc'] - $montantRegle, 2));
        $statut = $montantRegle > 0 ? 'partiellement réglée' : 'émise';

        $stockDeltas = $this->stockDeltasForInvoiceUpdate($current, $oldLines, $newLines);

        $this->db->beginTransaction();
        try {
            $stmt = $this->db->prepare("\n                UPDATE documents\n                SET client_id = :client_id, date_document = :date_document, date_echeance = :date_echeance,\n                    statut = :statut, total_ht = :total_ht, total_tva = :total_tva, total_ttc = :total_ttc,\n                    reste_du = :reste_du, marge_ht = :marge_ht, marge_pourcentage = :marge_pourcentage,\n                    observations = :observations\n                WHERE id = :id\n            ");
            $stmt->execute([
                'client_id' => $clientId,
                'date_document' => $dateDocument,
                'date_echeance' => $dateEcheance,
                'statut' => $statut,
                'total_ht' => $totals['total_ht'],
                'total_tva' => $totals['total_tva'],
                'total_ttc' => $totals['total_ttc'],
                'reste_du' => $resteDu,
                'marge_ht' => $totals['marge_ht'],
                'marge_pourcentage' => $totals['marge_pourcentage'],
                'observations' => trim((string)($data['observations'] ?? '')),
                'id' => $documentId,
            ]);

            $this->db->prepare('DELETE FROM documents_lignes WHERE document_id = :id')->execute(['id' => $documentId]);
            $this->insertLines($documentId, $newLines);
            $this->syncLegacyFacture($documentId);
            $this->db->commit();
        } catch (\Throwable $exception) {
            $this->db->rollBack();
            throw $exception;
        }

        $this->applyInvoiceStockDeltas($documentId, $stockDeltas);
    }

    private function stockDeltasForInvoiceUpdate(array $document, array $oldLines, array $newLines): array
    {
        // Une facture issue d'un BL ne touche pas au stock : le BL l'a déjà fait.
        $parentId = isset($document['parent_document_id']) ? (int)$document['parent_document_id'] : 0;
        if ($parentId > 0) {
            $parent = $this->findDocument($parentId);
            if ($parent && strtoupper((string)$parent['type_document']) === 'BL') {
                return [];
            }
        }

        // Compatibilité des factures historiques issues d'un BL lorsque le
        // document parent n'était pas encore présent dans la table commune.
        if ((string)($document['source_table'] ?? '') === 'factures' && (int)($document['source_id'] ?? 0) > 0) {
            $stmt = $this->db->prepare('SELECT bon_livraison_id FROM factures WHERE id = :id LIMIT 1');
            $stmt->execute(['id' => (int)$document['source_id']]);
            if ((int)($stmt->fetchColumn() ?: 0) > 0) {
                return [];
            }
        }

        $old = $this->aggregateQuantitiesByArticle($oldLines);
        $new = $this->aggregateQuantitiesByArticle($newLines);
        $articleIds = array_unique(array_merge(array_keys($old), array_keys($new)));
        $deltas = [];
        foreach ($articleIds as $articleId) {
            $delta = round(($new[$articleId] ?? 0.0) - ($old[$articleId] ?? 0.0), 2);
            if (abs($delta) > 0.00001) {
                $deltas[(int)$articleId] = $delta;
            }
        }
        return $deltas;
    }

    private function aggregateQuantitiesByArticle(array $lines): array
    {
        $result = [];
        foreach ($lines as $line) {
            $articleId = (int)($line['article_id'] ?? 0);
            if ($articleId <= 0) {
                continue;
            }
            $result[$articleId] = ($result[$articleId] ?? 0.0) + (float)($line['quantite'] ?? 0);
        }
        return $result;
    }

    private function applyInvoiceStockDeltas(int $documentId, array $deltas): void
    {
        if (empty($deltas)) {
            return;
        }
        $stock = new StockService();
        foreach ($deltas as $articleId => $delta) {
            if ($delta > 0) {
                $stock->sortie((int)$articleId, (float)$delta, 'FACTURE_EDIT', $documentId, 'Écart de stock après modification facture directe');
            } elseif ($delta < 0) {
                $stock->retour((int)$articleId, abs((float)$delta), 'FACTURE_EDIT', $documentId, 'Retour stock après modification facture directe');
            }
        }
    }

    private function syncLegacyFacture(int $documentId): int
    {
        $document = $this->findDocument($documentId);
        if (!$document || strtoupper((string)$document['type_document']) !== 'FACTURE') {
            return 0;
        }

        if (!$this->tableExists('factures') || !$this->tableExists('factures_lignes')) {
            // Les anciennes tables ne sont pas installées : Documents commerciaux reste la source.
            return 0;
        }

        $factureId = 0;
        if ((string)($document['source_table'] ?? '') === 'factures' && (int)($document['source_id'] ?? 0) > 0) {
            $factureId = (int)$document['source_id'];
        }

        if ($factureId <= 0) {
            $stmt = $this->db->prepare('SELECT id FROM factures WHERE numero = :numero AND actif = 1 LIMIT 1');
            $stmt->execute(['numero' => (string)$document['numero']]);
            $factureId = (int)($stmt->fetchColumn() ?: 0);
        }

        $bonLivraisonId = $this->legacyBonLivraisonIdFromParent($document);
        $payload = [
            'client_id' => (int)$document['client_id'],
            'bon_livraison_id' => $bonLivraisonId > 0 ? $bonLivraisonId : null,
            'numero' => (string)$document['numero'],
            'date_facture' => (string)$document['date_document'],
            'date_echeance' => $document['date_echeance'] ?: null,
            'statut' => (string)($document['statut'] ?: 'émise'),
            'total_ht' => (float)$document['total_ht'],
            'total_tva' => (float)$document['total_tva'],
            'total_ttc' => (float)$document['total_ttc'],
            'montant_regle' => (float)($document['montant_regle'] ?? 0),
            'reste_du' => (float)($document['reste_du'] ?? $document['total_ttc']),
            'marge_ht' => (float)($document['marge_ht'] ?? 0),
            'marge_pourcentage' => (float)($document['marge_pourcentage'] ?? 0),
            'observations' => (string)($document['observations'] ?? ''),
            'id' => $factureId,
        ];

        if ($factureId > 0) {
            $stmt = $this->db->prepare("\n                UPDATE factures\n                SET client_id = :client_id, bon_livraison_id = :bon_livraison_id, numero = :numero,\n                    date_facture = :date_facture, date_echeance = :date_echeance, statut = :statut,\n                    total_ht = :total_ht, total_tva = :total_tva, total_ttc = :total_ttc,\n                    montant_regle = :montant_regle, reste_du = :reste_du, marge_ht = :marge_ht,\n                    marge_pourcentage = :marge_pourcentage, observations = :observations\n                WHERE id = :id\n            ");
            $stmt->execute($payload);
        } else {
            $stmt = $this->db->prepare("\n                INSERT INTO factures (\n                    client_id, bon_livraison_id, numero, date_facture, date_echeance, statut,\n                    total_ht, total_tva, total_ttc, montant_regle, reste_du, marge_ht, marge_pourcentage, observations\n                ) VALUES (\n                    :client_id, :bon_livraison_id, :numero, :date_facture, :date_echeance, :statut,\n                    :total_ht, :total_tva, :total_ttc, :montant_regle, :reste_du, :marge_ht, :marge_pourcentage, :observations\n                )\n            ");
            $stmt->execute(array_diff_key($payload, ['id' => true]));
            $factureId = (int)$this->db->lastInsertId();
        }

        $this->db->prepare('DELETE FROM factures_lignes WHERE facture_id = :facture_id')->execute(['facture_id' => $factureId]);
        $lineStmt = $this->db->prepare("\n            INSERT INTO factures_lignes (\n                facture_id, article_id, reference, designation, unite, quantite,\n                prix_achat_ht, prix_vente_ht, remise, taux_tva, total_ht, total_tva, total_ttc,\n                marge_ht, marge_pourcentage, ordre\n            ) VALUES (\n                :facture_id, :article_id, :reference, :designation, :unite, :quantite,\n                :prix_achat_ht, :prix_vente_ht, :remise, :taux_tva, :total_ht, :total_tva, :total_ttc,\n                :marge_ht, :marge_pourcentage, :ordre\n            )\n        ");

        foreach ($this->lines($documentId) as $index => $line) {
            $lineStmt->execute([
                'facture_id' => $factureId,
                'article_id' => ((int)($line['article_id'] ?? 0)) > 0 ? (int)$line['article_id'] : null,
                'reference' => (string)($line['reference'] ?? ''),
                'designation' => (string)($line['designation'] ?? ''),
                'unite' => (string)($line['unite'] ?? 'unité'),
                'quantite' => (float)($line['quantite'] ?? 0),
                'prix_achat_ht' => (float)($line['prix_achat_ht'] ?? 0),
                'prix_vente_ht' => (float)($line['prix_vente_ht'] ?? 0),
                'remise' => (float)($line['remise'] ?? 0),
                'taux_tva' => (float)($line['taux_tva'] ?? 20),
                'total_ht' => (float)($line['total_ht'] ?? 0),
                'total_tva' => (float)($line['total_tva'] ?? 0),
                'total_ttc' => (float)($line['total_ttc'] ?? 0),
                'marge_ht' => (float)($line['marge_ht'] ?? 0),
                'marge_pourcentage' => (float)($line['marge_pourcentage'] ?? 0),
                'ordre' => $index + 1,
            ]);
        }

        $this->db->prepare("\n            UPDATE documents\n            SET source_table = 'factures', source_id = :source_id\n            WHERE id = :id\n        ")->execute([
            'source_id' => $factureId,
            'id' => $documentId,
        ]);

        return $factureId;
    }

    private function legacyBonLivraisonIdFromParent(array $document): ?int
    {
        $parentId = isset($document['parent_document_id']) ? (int)$document['parent_document_id'] : 0;
        if ($parentId <= 0) {
            return null;
        }
        $parent = $this->findDocument($parentId);
        if (!$parent || strtoupper((string)$parent['type_document']) !== 'BL') {
            return null;
        }
        if ((string)($parent['source_table'] ?? '') === 'bons_livraison' && (int)($parent['source_id'] ?? 0) > 0) {
            return (int)$parent['source_id'];
        }
        return null;
    }

    private function tableExists(string $table): bool
    {
        $stmt = $this->db->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = :table');
        $stmt->execute(['table' => $table]);
        return (int)$stmt->fetchColumn() > 0;
    }

    public function calculateTotals(array $lines): array
    {
        $totalHt = 0.0;
        $totalTva = 0.0;
        $totalTtc = 0.0;
        $margeHt = 0.0;

        foreach ($lines as $line) {
            $quantity = (float)($line['quantite'] ?? 0);
            $prixAchat = (float)($line['prix_achat_ht'] ?? 0);
            $prixVente = (float)($line['prix_vente_ht'] ?? 0);
            $remise = max(0, min(100, (float)($line['remise'] ?? 0)));
            $tva = (float)($line['taux_tva'] ?? 20);

            $lineHt = round($quantity * $prixVente * (1 - ($remise / 100)), 2);
            $lineTva = round($lineHt * ($tva / 100), 2);
            $lineTtc = round($lineHt + $lineTva, 2);
            $lineMarge = round($lineHt - ($quantity * $prixAchat), 2);

            $totalHt += $lineHt;
            $totalTva += $lineTva;
            $totalTtc += $lineTtc;
            $margeHt += $lineMarge;
        }

        return [
            'total_ht' => round($totalHt, 2),
            'total_tva' => round($totalTva, 2),
            'total_ttc' => round($totalTtc, 2),
            'marge_ht' => round($margeHt, 2),
            'marge_pourcentage' => $totalHt > 0 ? round(($margeHt / $totalHt) * 100, 2) : 0,
        ];
    }

    public function legacyUrl(array $document): ?string
    {
        $sourceTable = (string)($document['source_table'] ?? '');
        $sourceId = (int)($document['source_id'] ?? 0);

        return match ($sourceTable) {
            'devis' => 'index.php?page=devis_show&id=' . $sourceId,
            'bons_livraison' => 'index.php?page=bons_livraison_show&id=' . $sourceId,
            'factures' => 'index.php?page=factures_show&id=' . $sourceId,
            default => null,
        };
    }

    public function canTransform(string $sourceType, string $targetType): bool
    {
        $sourceType = strtoupper($sourceType);
        $targetType = strtoupper($targetType);
        return match ($sourceType) {
            'DEVIS' => in_array($targetType, ['BL', 'FACTURE'], true),
            'BL' => $targetType === 'FACTURE',
            'FACTURE' => false,
            default => false,
        };
    }

    public function transformTargets(string $sourceType): array
    {
        $targets = [];
        foreach ($this->allowedTypes() as $type) {
            if ($this->canTransform($sourceType, $type)) {
                $targets[$type] = $this->typeLabel($type);
            }
        }
        return $targets;
    }

    private function normalizeLines(array $rawLines, int $clientId = 0): array
    {
        $lines = [];
        foreach ($rawLines as $line) {
            $designation = trim((string)($line['designation'] ?? ''));
            $quantity = $this->number($line['quantite'] ?? 0);
            if ($designation === '' || $quantity <= 0) {
                continue;
            }

            $articleId = (int)($line['article_id'] ?? 0);
            $prixAchat = $this->number($line['prix_achat_ht'] ?? 0);
            $prixVente = $this->number($line['prix_vente_ht'] ?? 0);

            // Sécurité serveur : si aucun prix n'est envoyé, on applique le tarif client.
            // Si un prix est envoyé depuis le formulaire, il est conservé afin de permettre
            // une modification manuelle ponctuelle dans le document.
            if ($prixVente <= 0 && $clientId > 0 && $articleId > 0) {
                $prixVente = $this->tarifClientService->getPrixClient($clientId, $articleId, 0);
            }

            $remise = max(0, min(100, $this->number($line['remise'] ?? 0)));
            $tva = $this->number($line['taux_tva'] ?? 20);
            $totalHt = round($quantity * $prixVente * (1 - ($remise / 100)), 2);
            $totalTva = round($totalHt * ($tva / 100), 2);
            $totalTtc = round($totalHt + $totalTva, 2);
            $margeHt = round($totalHt - ($quantity * $prixAchat), 2);
            $margePourcentage = $totalHt > 0 ? round(($margeHt / $totalHt) * 100, 2) : 0;

            $lines[] = [
                'article_id' => $articleId,
                'reference' => trim((string)($line['reference'] ?? '')),
                'designation' => $designation,
                'unite' => trim((string)($line['unite'] ?? 'unité')) ?: 'unité',
                'quantite' => $quantity,
                'prix_achat_ht' => $prixAchat,
                'prix_vente_ht' => $prixVente,
                'remise' => $remise,
                'taux_tva' => $tva,
                'total_ht' => $totalHt,
                'total_tva' => $totalTva,
                'total_ttc' => $totalTtc,
                'marge_ht' => $margeHt,
                'marge_pourcentage' => $margePourcentage,
            ];
        }
        return $lines;
    }

    private function insertLines(int $documentId, array $lines): void
    {
        $stmt = $this->db->prepare("\n            INSERT INTO documents_lignes (\n                document_id, article_id, reference, designation, unite, quantite,\n                prix_achat_ht, prix_vente_ht, remise, taux_tva,\n                total_ht, total_tva, total_ttc, marge_ht, marge_pourcentage, ordre\n            ) VALUES (\n                :document_id, :article_id, :reference, :designation, :unite, :quantite,\n                :prix_achat_ht, :prix_vente_ht, :remise, :taux_tva,\n                :total_ht, :total_tva, :total_ttc, :marge_ht, :marge_pourcentage, :ordre\n            )\n        ");

        foreach ($lines as $index => $line) {
            $stmt->execute([
                'document_id' => $documentId,
                'article_id' => $line['article_id'] ?: null,
                'reference' => $line['reference'],
                'designation' => $line['designation'],
                'unite' => $line['unite'],
                'quantite' => $line['quantite'],
                'prix_achat_ht' => $line['prix_achat_ht'],
                'prix_vente_ht' => $line['prix_vente_ht'],
                'remise' => $line['remise'],
                'taux_tva' => $line['taux_tva'],
                'total_ht' => $line['total_ht'],
                'total_tva' => $line['total_tva'],
                'total_ttc' => $line['total_ttc'],
                'marge_ht' => $line['marge_ht'],
                'marge_pourcentage' => $line['marge_pourcentage'],
                'ordre' => $index + 1,
            ]);
        }
    }

    private function defaultStatus(string $type): string
    {
        return match (strtoupper($type)) {
            'DEVIS' => 'brouillon',
            'BL' => 'à livrer',
            'FACTURE' => 'émise',
            'AVOIR' => 'émis',
            default => 'brouillon',
        };
    }

    private function findDocument(int $id): ?array
    {
        $stmt = $this->db->prepare('SELECT * FROM documents WHERE id = :id AND actif = 1 LIMIT 1');
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    private function lines(int $documentId): array
    {
        $stmt = $this->db->prepare('SELECT * FROM documents_lignes WHERE document_id = :id ORDER BY ordre ASC, id ASC');
        $stmt->execute(['id' => $documentId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    private function findChildByType(int $parentDocumentId, string $targetType): ?array
    {
        $stmt = $this->db->prepare('SELECT * FROM documents WHERE parent_document_id = :parent AND type_document = :type AND actif = 1 LIMIT 1');
        $stmt->execute(['parent' => $parentDocumentId, 'type' => strtoupper($targetType)]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    private function markSourceTransformed(int $sourceDocumentId, string $targetType): void
    {
        $source = $this->findDocument($sourceDocumentId);
        if (!$source) {
            return;
        }
        $newStatus = match (strtoupper($targetType)) {
            'BL' => 'transformé en BL',
            'FACTURE' => 'facturé',
            'AVOIR' => 'avoir créé',
            default => (string)$source['statut'],
        };
        $this->db->prepare('UPDATE documents SET statut = :statut WHERE id = :id')->execute([
            'statut' => $newStatus,
            'id' => $sourceDocumentId,
        ]);
    }

    private function shouldDecrementStock(string $type, ?int $parentDocumentId): bool
    {
        $type = strtoupper($type);
        if ($type === 'BL') {
            return true;
        }
        if ($type === 'FACTURE') {
            if ($parentDocumentId === null) {
                return true;
            }
            $parent = $this->findDocument($parentDocumentId);
            return !$parent || strtoupper((string)$parent['type_document']) !== 'BL';
        }
        return false;
    }

    private function decrementStockForDocument(int $documentId, string $type): void
    {
        $stock = new StockService();
        foreach ($this->lines($documentId) as $line) {
            $articleId = (int)($line['article_id'] ?? 0);
            $quantity = (float)($line['quantite'] ?? 0);
            if ($articleId > 0 && $quantity > 0) {
                $stock->sortie($articleId, $quantity, 'DOCUMENT_' . strtoupper($type), $documentId, 'Sortie automatique document ' . strtoupper($type));
            }
        }
    }

    private function number(mixed $value): float
    {
        if (is_numeric($value)) {
            return (float)$value;
        }
        return (float)str_replace(',', '.', preg_replace('/[^0-9,.-]/', '', (string)$value));
    }
}
