<?php

declare(strict_types=1);

namespace OCA\Mitgliederverwaltung\Service;

use DateTime;
use OCA\Mitgliederverwaltung\Db\FeeRecord;
use OCA\Mitgliederverwaltung\Db\FeeRecordMapper;
use OCA\Mitgliederverwaltung\Db\FeeRule;
use OCA\Mitgliederverwaltung\Db\FeeRuleMapper;
use OCA\Mitgliederverwaltung\Db\Member;
use OCA\Mitgliederverwaltung\Db\MemberMapper;
use OCA\Mitgliederverwaltung\Service\AuditService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\Exception;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;

/**
 * Fee calculation engine with family discount DSL, frozen rates, and batch processing.
 *
 * Core logic:
 * 1. Erziehungsberechtigter -> skip (no fee)
 * 2. Inactive member with frozen_fee_rate -> use frozen rate
 * 3. Active children in family sorted by age -> position-based family discount
 * 4. Members without family -> use base_rate
 *
 * Part of Issue #42.
 */
class FeeCalculationService {

    private FeeRuleMapper $ruleMapper;
    private FeeRecordMapper $recordMapper;
    private MemberMapper $memberMapper;
    private AuditService $auditService;
    private IUserSession $userSession;
    private LoggerInterface $logger;

    public function __construct(
        FeeRuleMapper $ruleMapper,
        FeeRecordMapper $recordMapper,
        MemberMapper $memberMapper,
        AuditService $auditService,
        IUserSession $userSession,
        LoggerInterface $logger
    ) {
        $this->ruleMapper = $ruleMapper;
        $this->recordMapper = $recordMapper;
        $this->memberMapper = $memberMapper;
        $this->auditService = $auditService;
        $this->userSession = $userSession;
        $this->logger = $logger;
    }

    // ── Core fee calculation (pure function) ────────────────────────

    /**
     * Calculate the fee for a single member based on the applicable rule.
     *
     * @param Member $member The member to calculate for
     * @param Member[] $familyMembers All members in the same family (empty if no family)
     * @param FeeRule $rule The applicable fee rule
     * @return string The calculated fee amount as a decimal string
     */
    public function calculateFee(Member $member, array $familyMembers, FeeRule $rule): string {
        // 1. Erziehungsberechtigter pays nothing
        if ($member->getRolle() === 'erziehungsberechtigter') {
            return '0.00';
        }

        // 2. Inactive member with frozen rate
        if ($member->getStatus() === 'inaktiv' && $member->getFrozenFeeRate() !== null) {
            return $member->getFrozenFeeRate();
        }

        // 3. Member without family -> base rate
        if ($member->getFamilyId() === null || empty($familyMembers)) {
            return $rule->getBaseRate();
        }

        // 4. Family discount based on child position
        $activeChildren = $this->getActiveChildrenSortedByAge($familyMembers);

        if (empty($activeChildren)) {
            return $rule->getBaseRate();
        }

        // Find the member's position among active children (1-indexed)
        $position = $this->findChildPosition($member, $activeChildren);

        if ($position === 0) {
            // Member is not an active child in the family (e.g. inactive but not frozen)
            return $rule->getBaseRate();
        }

        return $this->applyFamilyRule($position, $rule);
    }

    /**
     * Filter and sort family members: only active children (rolle=mitglied, status=aktiv),
     * sorted by geburtsdatum ASC (oldest first).
     *
     * @param Member[] $familyMembers
     * @return Member[]
     */
    private function getActiveChildrenSortedByAge(array $familyMembers): array {
        $activeChildren = array_filter($familyMembers, function (Member $m) {
            return $m->getRolle() === 'mitglied' && $m->getStatus() === 'aktiv';
        });

        usort($activeChildren, function (Member $a, Member $b) {
            // Members without birthday sort last
            $bdA = $a->getGeburtsdatum() ?? '9999-12-31';
            $bdB = $b->getGeburtsdatum() ?? '9999-12-31';
            return strcmp($bdA, $bdB);
        });

        return array_values($activeChildren);
    }

    /**
     * Find 1-indexed position of a member among sorted active children.
     * Returns 0 if member is not found among active children.
     */
    private function findChildPosition(Member $member, array $activeChildren): int {
        foreach ($activeChildren as $index => $child) {
            if ($child->getId() === $member->getId()) {
                return $index + 1;
            }
        }
        return 0;
    }

    /**
     * Apply the family rule for a given child position.
     * Family rules JSON format: {"1": 100.00, "2": 75.00, "3+": 50.00}
     * Falls back to base_rate if no matching rule.
     */
    private function applyFamilyRule(int $position, FeeRule $rule): string {
        $familyRules = $rule->getFamilyRules();

        // Exact position match
        $posKey = (string)$position;
        if (isset($familyRules[$posKey])) {
            return number_format((float)$familyRules[$posKey], 2, '.', '');
        }

        // Check for "N+" catch-all rules, starting from the position downward
        for ($i = $position; $i >= 1; $i--) {
            $catchAllKey = $i . '+';
            if (isset($familyRules[$catchAllKey])) {
                return number_format((float)$familyRules[$catchAllKey], 2, '.', '');
            }
        }

        // Fallback to base rate
        return $rule->getBaseRate();
    }

    // ── Batch calculation ───────────────────────────────────────────

    /**
     * Run batch fee calculation for an entire year.
     * - Finds the applicable rule for the year
     * - Iterates all active + paying members
     * - Skips records with manuell_angepasst = true
     * - Creates/updates fee records with rule snapshot
     *
     * @return array{created: int, updated: int, skipped: int}
     * @throws DoesNotExistException if no rule exists for the year
     * @throws Exception
     */
    public function batchCalculate(int $year): array {
        $rule = $this->ruleMapper->findByYear($year);

        // Get all non-deleted members
        $allMembers = $this->memberMapper->findAll();

        // Group members by family
        $familyGroups = $this->groupMembersByFamily($allMembers);

        $created = 0;
        $updated = 0;
        $skipped = 0;
        $now = (new DateTime())->format('Y-m-d H:i:s');

        foreach ($allMembers as $member) {
            // Only calculate for active members and paying members
            if (!$this->isPayingMember($member)) {
                continue;
            }

            // Check if a manually adjusted record already exists
            try {
                $existing = $this->recordMapper->findByMemberAndYear($member->getId(), $year);
                if ($existing->getManuellAngepasst()) {
                    $skipped++;
                    continue;
                }
            } catch (DoesNotExistException $e) {
                $existing = null;
            }

            // Get family members for this member
            $familyMembers = [];
            $familyId = $member->getFamilyId();
            if ($familyId !== null && isset($familyGroups[$familyId])) {
                $familyMembers = $familyGroups[$familyId];
            }

            $amount = $this->calculateFee($member, $familyMembers, $rule);
            $ruleSnapshot = json_encode($rule->jsonSerialize());

            if ($existing !== null) {
                // Update existing record
                $existing->setAmount($amount);
                $existing->setRuleSnapshotJson($ruleSnapshot);
                $existing->setUpdatedAt($now);
                $this->recordMapper->update($existing);
                $updated++;
            } else {
                // Create new record
                $record = new FeeRecord();
                $record->setMemberId($member->getId());
                $record->setYear($year);
                $record->setAmount($amount);
                $record->setRuleSnapshotJson($ruleSnapshot);
                $record->setManuellAngepasst(false);
                $record->setPaid(false);
                $record->setCreatedAt($now);
                $record->setUpdatedAt($now);
                $this->recordMapper->insert($record);
                $created++;
            }
        }

        $this->logger->info('Batch fee calculation completed', [
            'year' => $year,
            'created' => $created,
            'updated' => $updated,
            'skipped' => $skipped,
            'app' => 'mitgliederverwaltung',
        ]);

        return [
            'created' => $created,
            'updated' => $updated,
            'skipped' => $skipped,
        ];
    }

    /**
     * Determine if a member should have fees calculated.
     * Active members and inactive members with frozen rates are paying members.
     * Erziehungsberechtigter always excluded.
     */
    private function isPayingMember(Member $member): bool {
        if ($member->getRolle() === 'erziehungsberechtigter') {
            return false;
        }

        $status = $member->getStatus();
        if ($status === 'aktiv') {
            return true;
        }

        // Inactive members with frozen rate still pay
        if ($status === 'inaktiv' && $member->getFrozenFeeRate() !== null) {
            return true;
        }

        return false;
    }

    /**
     * Group members by their family ID.
     *
     * @param Member[] $members
     * @return array<int, Member[]>
     */
    private function groupMembersByFamily(array $members): array {
        $groups = [];
        foreach ($members as $member) {
            $familyId = $member->getFamilyId();
            if ($familyId !== null) {
                $groups[$familyId][] = $member;
            }
        }
        return $groups;
    }

    // ── Query methods ───────────────────────────────────────────────

    /**
     * Find the applicable fee rule for a year.
     *
     * @throws DoesNotExistException
     * @throws Exception
     */
    public function getRulesForYear(int $year): FeeRule {
        return $this->ruleMapper->findByYear($year);
    }

    /**
     * Get all fee rules.
     *
     * @return FeeRule[]
     * @throws Exception
     */
    public function getAllRules(): array {
        return $this->ruleMapper->findAll();
    }

    /**
     * Get all fee records for a year.
     *
     * @return FeeRecord[]
     * @throws Exception
     */
    public function getFeeRecordsForYear(int $year): array {
        return $this->recordMapper->findByYear($year);
    }

    /**
     * Get fee history for a member.
     *
     * @return FeeRecord[]
     * @throws Exception
     */
    public function getMemberFeeHistory(int $memberId): array {
        return $this->recordMapper->findByMemberId($memberId);
    }

    // ── Record management ───────────────────────────────────────────

    /**
     * Mark a fee record as paid.
     *
     * @throws DoesNotExistException
     * @throws MultipleObjectsReturnedException
     * @throws Exception
     */
    public function markAsPaid(int $recordId, string $paymentDate): FeeRecord {
        $record = $this->recordMapper->findById($recordId);
        $record->setPaid(true);
        $record->setPaymentDate($paymentDate);
        $record->setUpdatedAt((new DateTime())->format('Y-m-d H:i:s'));

        /** @var FeeRecord $record */
        return $this->recordMapper->update($record);
    }

    /**
     * Manually override a fee record's amount.
     *
     * @throws DoesNotExistException
     * @throws MultipleObjectsReturnedException
     * @throws Exception
     */
    public function manualOverride(int $recordId, string $amount, ?string $notes = null): FeeRecord {
        $record = $this->recordMapper->findById($recordId);
        $record->setAmount($amount);
        $record->setManuellAngepasst(true);
        if ($notes !== null) {
            $record->setNotes($notes);
        }
        $record->setUpdatedAt((new DateTime())->format('Y-m-d H:i:s'));

        /** @var FeeRecord $record */
        return $this->recordMapper->update($record);
    }

    // ── Rule management ─────────────────────────────────────────────

    /**
     * Create a new fee rule.
     *
     * @throws ValidationException
     * @throws Exception
     */
    public function createRule(array $data): FeeRule {
        $this->validateRuleData($data);

        $now = (new DateTime())->format('Y-m-d H:i:s');
        $userId = $this->getCurrentUserId();

        $rule = new FeeRule();
        $rule->setYearFrom((int)$data['yearFrom']);
        $rule->setBaseRate(number_format((float)$data['baseRate'], 2, '.', ''));
        $rule->setFamilyRulesJson(
            is_string($data['familyRulesJson'] ?? null)
                ? $data['familyRulesJson']
                : json_encode($data['familyRules'] ?? [])
        );
        $rule->setInactiveRule($data['inactiveRule'] ?? 'freeze_rate_at_exit');
        $rule->setCreatedAt($now);
        $rule->setCreatedBy($userId);

        /** @var FeeRule $rule */
        $rule = $this->ruleMapper->insert($rule);

        $this->auditService->logCreate($rule->jsonSerialize(), 'fee_rule', $rule->getId());

        return $rule;
    }

    /**
     * Update an existing fee rule.
     *
     * @throws DoesNotExistException
     * @throws MultipleObjectsReturnedException
     * @throws ValidationException
     * @throws Exception
     */
    public function updateRule(int $id, array $data): FeeRule {
        $rule = $this->ruleMapper->findById($id);
        $oldData = $rule->jsonSerialize();

        if (isset($data['yearFrom'])) {
            $rule->setYearFrom((int)$data['yearFrom']);
        }
        if (isset($data['baseRate'])) {
            $rule->setBaseRate(number_format((float)$data['baseRate'], 2, '.', ''));
        }
        if (isset($data['familyRulesJson'])) {
            $rule->setFamilyRulesJson($data['familyRulesJson']);
        } elseif (isset($data['familyRules'])) {
            $rule->setFamilyRulesJson(json_encode($data['familyRules']));
        }
        if (isset($data['inactiveRule'])) {
            $rule->setInactiveRule($data['inactiveRule']);
        }

        /** @var FeeRule $rule */
        $rule = $this->ruleMapper->update($rule);

        $this->auditService->logUpdate($oldData, $rule->jsonSerialize(), 'fee_rule', $id);

        return $rule;
    }

    // ── Helpers ──────────────────────────────────────────────────────

    /**
     * @throws ValidationException
     */
    private function validateRuleData(array $data): void {
        if (!isset($data['yearFrom'])) {
            throw new ValidationException('yearFrom ist erforderlich.');
        }
        if (!isset($data['baseRate'])) {
            throw new ValidationException('baseRate ist erforderlich.');
        }
        if ((float)$data['baseRate'] < 0) {
            throw new ValidationException('baseRate darf nicht negativ sein.');
        }
    }

    private function getCurrentUserId(): string {
        $user = $this->userSession->getUser();
        return $user ? $user->getUID() : 'system';
    }
}
