<?php

namespace Modules\LandedCost\Entities\Services;

use Illuminate\Support\Facades\DB;
use Modules\LandedCost\Entities\LcCostSnapshot;
use Modules\LandedCost\Entities\Drivers\LandedCostDriverInterface;

/**
 * Applies the computed landed unit cost into the REAL cost basis (purchase_lines +
 * product variations) and can roll it back exactly.
 *
 * Invariant: lc_cost_snapshots always holds the pre-landed (original) per-line and
 * per-variation costs. "Applied" ⇔ snapshot rows exist for the parent. Landed cost is
 * always recomputed from the original base (never compounded). All mutations go through
 * Eloquent model instances so the ActivityLog observer records before/after.
 */
class LandedCostApplicationService
{
    /** Whether landed cost is currently applied to this parent. */
    public function isApplied(LandedCostDriverInterface $driver): bool
    {
        return LcCostSnapshot::forParent($driver->parentType(), $driver->parentId())->exists();
    }

    /**
     * Explicit "Apply": snapshot originals (first time) and overwrite real costs.
     * Idempotent — if already applied it restores to original first, then re-overwrites.
     */
    public function apply(LandedCostDriverInterface $driver): array
    {
        $summary = DB::transaction(function () use ($driver) {
            if ($this->isApplied($driver)) {
                $this->restore($driver);
            } else {
                $this->snapshot($driver);
            }

            return $this->overwrite($driver);
        });

        // Business-level audit after commit (row-level before/after is captured by the observer).
        $this->logBusiness($driver, 'landed_cost_applied', ['applied' => false], ['applied' => true] + $summary);

        return ['applied' => true] + $summary;
    }

    /**
     * Auto re-apply after an expense change while applied:
     * restore originals → recompute landed (callback) → overwrite again.
     * Snapshot is untouched (still the originals).
     */
    public function reapply(LandedCostDriverInterface $driver, callable $recalc): array
    {
        return DB::transaction(function () use ($driver, $recalc) {
            $this->restore($driver);   // lines back to original base
            $recalc();                 // driver->recalculateLandedCosts() from original base
            $summary = $this->overwrite($driver);
            return ['applied' => true] + $summary;
        });
    }

    /** Restore real costs from the snapshot and drop it (→ un-applied). */
    public function rollback(LandedCostDriverInterface $driver): array
    {
        $count = DB::transaction(function () use ($driver) {
            $this->restore($driver);
            return $this->clearSnapshots($driver);
        });

        // Business-level audit after commit (row-level before/after is captured by the observer).
        // NB: enhanced_activity_logs.action is varchar(20) — keep action names <= 20 chars.
        $this->logBusiness($driver, 'landed_cost_revert', ['applied' => true], ['applied' => false, 'restored_lines' => $count]);

        return ['applied' => false, 'restored_lines' => $count];
    }

    // ── primitives ────────────────────────────────────────────────────────────

    /** Capture original line + variation costs (only for lines not already snapshotted). */
    protected function snapshot(LandedCostDriverInterface $driver): void
    {
        $pt = $driver->parentType();
        $pid = $driver->parentId();
        $now = now();
        $uid = auth()->id();

        foreach ($driver->getLines() as $line) {
            $exists = LcCostSnapshot::forParent($pt, $pid)
                ->where('purchase_line_id', $line->id)->exists();
            if ($exists) {
                continue;
            }

            $v = $line->variations;
            LcCostSnapshot::create([
                'parent_type'                     => $pt,
                'parent_id'                       => $pid,
                'purchase_line_id'                => $line->id,
                'variation_id'                    => $line->variation_id,
                'orig_purchase_price'             => (float) $line->purchase_price,
                'orig_purchase_price_inc_tax'     => (float) $line->purchase_price_inc_tax,
                'orig_item_tax'                   => (float) $line->item_tax,
                'orig_pp_without_discount'        => (float) $line->pp_without_discount,
                'orig_var_default_purchase_price' => $v ? (float) $v->default_purchase_price : null,
                'orig_var_dpp_inc_tax'            => $v ? (float) $v->dpp_inc_tax : null,
                'orig_var_profit_percent'         => $v ? (float) $v->profit_percent : null,
                'applied_at'                      => $now,
                'applied_by'                      => $uid,
            ]);
        }
    }

    /**
     * Overwrite each line's cost (and its variation cost) with the landed unit cost.
     * Assumes the line currently holds the original/base cost (delta is added once).
     */
    protected function overwrite(LandedCostDriverInterface $driver): array
    {
        $landed = $driver->getLandedCostData();   // [lineId => ['landed_unit_cost_base' => x]]
        $affected = 0;
        $totalExpenseBase = 0.0;

        foreach ($driver->getLines() as $line) {
            $landedUnit = $landed[$line->id]['landed_unit_cost_base'] ?? null;
            if ($landedUnit === null) {
                continue;
            }

            $delta = round((float) $landedUnit - (float) $line->purchase_price_inc_tax, 4);
            if (abs($delta) < 0.00005) {
                continue; // no expense allocated to this line
            }

            // Expenses are not taxed: add the per-unit delta to ex-tax price and inc-tax price.
            $line->purchase_price          = round((float) $line->purchase_price + $delta, 4);
            $line->purchase_price_inc_tax  = round((float) $line->purchase_price_inc_tax + $delta, 4);
            $line->pp_without_discount     = round((float) $line->pp_without_discount + $delta, 4);
            $line->save();

            $v = $line->variations;
            if ($v) {
                $newDpp = round((float) $v->default_purchase_price + $delta, 4);
                $sell   = (float) $v->default_sell_price;
                $v->default_purchase_price = $newDpp;
                $v->dpp_inc_tax            = round($newDpp + (float) $line->item_tax, 4);
                $v->profit_percent         = $newDpp == 0.0 ? 0 : round((($sell - $newDpp) / $newDpp) * 100, 4);
                $v->save();
            }

            $affected++;
            $totalExpenseBase += $delta * (float) $line->quantity;
        }

        return ['affected_lines' => $affected, 'applied_expense_base' => round($totalExpenseBase, 4)];
    }

    /** Write line + variation costs back to the snapshot originals. */
    protected function restore(LandedCostDriverInterface $driver): void
    {
        $snaps = LcCostSnapshot::forParent($driver->parentType(), $driver->parentId())
            ->get()->keyBy('purchase_line_id');

        foreach ($driver->getLines() as $line) {
            $s = $snaps[$line->id] ?? null;
            if (!$s) {
                continue;
            }

            $line->purchase_price         = (float) $s->orig_purchase_price;
            $line->purchase_price_inc_tax = (float) $s->orig_purchase_price_inc_tax;
            $line->item_tax               = (float) $s->orig_item_tax;
            $line->pp_without_discount    = (float) $s->orig_pp_without_discount;
            $line->save();

            $v = $line->variations;
            if ($v && $s->orig_var_default_purchase_price !== null) {
                $v->default_purchase_price = (float) $s->orig_var_default_purchase_price;
                $v->dpp_inc_tax            = (float) $s->orig_var_dpp_inc_tax;
                $v->profit_percent         = (float) $s->orig_var_profit_percent;
                $v->save();
            }
        }
    }

    /** Delete snapshot rows (via model instances so deletions are audited). */
    protected function clearSnapshots(LandedCostDriverInterface $driver): int
    {
        $snaps = LcCostSnapshot::forParent($driver->parentType(), $driver->parentId())->get();
        $count = $snaps->count();
        foreach ($snaps as $s) {
            $s->delete();
        }
        return $count;
    }

    /** Named business-level audit entry (no-op if ActivityLog is absent). */
    protected function logBusiness(LandedCostDriverInterface $driver, string $action, array $before, array $after): void
    {
        $serviceClass = \Modules\ActivityLog\Entities\Services\ActivityLoggerService::class;
        if (!class_exists($serviceClass)) {
            return;
        }
        $subject = $driver->subjectModel();
        if (!$subject) {
            return;
        }

        try {
            app($serviceClass)->logCustom($subject, $action, $before, $after);
        } catch (\Throwable $e) {
            // auditing must never break the operation
            \Log::warning('LandedCost logBusiness failed: ' . $e->getMessage());
        }
    }
}
