<?php

namespace MintyPHP\Module\Security\Service;

use MintyPHP\Module\Security\Repository\SecurityCheckRepository;

/**
 * Orchestrates security checks: creation from the wizard, checklist state updates,
 * status derivation, archival and listing.
 *
 * The fixed process definition + progress/status math live in SecurityCheckProcess;
 * the per-product technical checklist is frozen onto the check at creation time.
 *
 * @api Consumed by Security check pages (list, create wizard, workspace).
 */
class SecurityCheckService
{
    public function __construct(
        private readonly SecurityCheckRepository $repository,
        private readonly SecurityCheckTemplateService $templateService,
        private readonly SecurityCheckProcess $process
    ) {
    }

    /**
     * Create a check from the wizard selection (customer + domain + product/template).
     *
     * @param array<string, mixed> $input
     * @return array{ok: bool, id?: int, errors: array<string, string>}
     */
    public function create(int $tenantId, array $input, int $actorUserId): array
    {
        $debitorNo = trim((string) ($input['debitor_no'] ?? ''));
        $debitorName = trim((string) ($input['debitor_name'] ?? ''));
        $domainNo = trim((string) ($input['domain_no'] ?? ''));
        $domainUrl = trim((string) ($input['domain_url'] ?? ''));
        $productCode = trim((string) ($input['product_code'] ?? ''));
        $ownerUserId = (int) ($input['owner_user_id'] ?? $actorUserId);

        $errors = [];
        if ($tenantId <= 0) {
            $errors['tenant'] = t('No tenant context');
        }
        if ($debitorNo === '') {
            $errors['debitor_no'] = t('Please select a customer');
        }
        if ($domainNo === '') {
            $errors['domain_no'] = t('Please select a domain');
        }

        $template = null;
        if ($productCode === '') {
            $errors['product_code'] = t('Please select a software product');
        } else {
            $template = $this->templateService->findByCode($tenantId, $productCode);
            if ($template === null || (int) ($template['active'] ?? 0) !== 1) {
                $errors['product_code'] = t('Selected check template not found');
            }
        }

        if ($errors !== []) {
            return ['ok' => false, 'errors' => $errors];
        }

        /** @var array<string, mixed> $template */
        $schema = $this->templateService->decodeSchema($template);

        $id = $this->repository->insert($tenantId, [
            'debitor_no' => $debitorNo,
            'debitor_name' => $debitorName,
            'domain_no' => $domainNo,
            'domain_url' => $domainUrl,
            'product_code' => $productCode,
            'product_name' => (string) ($template['product_name'] ?? $productCode),
            'template_id' => (int) ($template['id'] ?? 0),
            'owner_user_id' => $ownerUserId > 0 ? $ownerUserId : null,
            'status' => SecurityCheckProcess::STATUS_OPEN,
            'process_state_json' => json_encode((object) []),
            'tech_schema_snapshot_json' => json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'tech_state_json' => json_encode((object) []),
            'created_by' => $actorUserId,
        ]);

        if ($id === null) {
            return ['ok' => false, 'errors' => ['general' => t('Security check could not be created')]];
        }

        return ['ok' => true, 'id' => $id, 'errors' => []];
    }

    /**
     * Find a check enriched with decoded state, the frozen tech checklist and progress.
     *
     * @return array<string, mixed>|null
     */
    public function findById(int $tenantId, int $id): ?array
    {
        $row = $this->repository->findById($tenantId, $id);
        if ($row === null) {
            return null;
        }

        $row['process_state'] = $this->decode($row['process_state_json'] ?? '');
        $row['tech_schema_snapshot'] = $this->decode($row['tech_schema_snapshot_json'] ?? '');
        $row['tech_state'] = $this->decode($row['tech_state_json'] ?? '');
        $row['tech_items'] = SecurityCheckProcess::techCheckItems($row['tech_schema_snapshot']);
        $row['progress'] = $this->process->computeProgress(
            $row['process_state'],
            $row['tech_schema_snapshot'],
            $row['tech_state'],
            (string) ($row['status'] ?? '') === SecurityCheckProcess::STATUS_ARCHIVED
        );

        return $row;
    }

    /**
     * Save the whole checklist workspace form (steps + info fields + tech items).
     * Recomputes derived status. Stamps who/when on each item that newly turns done.
     *
     * @param array{step?: array<string, mixed>, info?: array<string, mixed>, tech?: array<string, mixed>} $input
     * @return array{ok: bool, errors: array<string, string>, warning?: string}
     */
    public function saveState(int $tenantId, int $id, array $input, int $actorUserId): array
    {
        $row = $this->repository->findById($tenantId, $id);
        if ($row === null) {
            return ['ok' => false, 'errors' => ['general' => t('Security check not found')]];
        }
        if ((string) ($row['status'] ?? '') === SecurityCheckProcess::STATUS_ARCHIVED) {
            return ['ok' => false, 'errors' => ['general' => t('Archived checks cannot be edited')]];
        }

        $prevProcess = $this->decode($row['process_state_json'] ?? '');
        $prevTech = $this->decode($row['tech_state_json'] ?? '');
        $techSnapshot = $this->decode($row['tech_schema_snapshot_json'] ?? '');
        $now = date('Y-m-d H:i:s');

        $stepInput = is_array($input['step'] ?? null) ? $input['step'] : [];
        $techInput = is_array($input['tech'] ?? null) ? $input['tech'] : [];

        // Each step may carry structured fields (info textareas, datetimes, …).
        // A step can only be completed once all of its required fields are filled in
        // (e.g. "Special notes" and the free-text note stay optional).
        $anyStepGated = false;
        $newProcess = [];
        foreach ($this->process->manualStepKeys() as $key) {
            $fieldDefs = $this->process->stepFields($key);
            $submittedFields = is_array($stepInput[$key]['fields'] ?? null) ? $stepInput[$key]['fields'] : [];
            $fieldValues = [];
            foreach ($fieldDefs as $field) {
                $fieldValues[$field['key']] = trim((string) ($submittedFields[$field['key']] ?? ''));
            }

            $requirementsMet = true;
            foreach ($this->process->requiredFieldKeys($key) as $requiredKey) {
                if (($fieldValues[$requiredKey] ?? '') === '') {
                    $requirementsMet = false;
                    break;
                }
            }

            $done = ($stepInput[$key]['done'] ?? '') !== '';
            if ($done && !$requirementsMet) {
                $done = false;
                $anyStepGated = true;
            }

            $note = trim((string) ($stepInput[$key]['note'] ?? ''));
            $entry = is_array($prevProcess[$key] ?? null) ? $prevProcess[$key] : [];
            $entry = $this->applyCompletion($entry, $done, $actorUserId, $now);
            $entry['note'] = $note;
            if ($fieldDefs !== []) {
                $entry['fields'] = $fieldValues;
            }
            $newProcess[$key] = $entry;
        }

        $newTech = [];
        foreach (SecurityCheckProcess::techCheckItems($techSnapshot) as $item) {
            $key = $item['key'];
            $done = ($techInput[$key]['done'] ?? '') !== '';
            $note = trim((string) ($techInput[$key]['note'] ?? ''));
            $entry = is_array($prevTech[$key] ?? null) ? $prevTech[$key] : [];
            $entry = $this->applyCompletion($entry, $done, $actorUserId, $now);
            $entry['note'] = $note;
            $newTech[$key] = $entry;
        }

        $progress = $this->process->computeProgress($newProcess, $techSnapshot, $newTech, false);

        $ok = $this->repository->updateState($tenantId, $id, [
            'process_state_json' => json_encode($newProcess, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'tech_state_json' => json_encode($newTech, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            'status' => $progress['status'],
            'updated_by' => $actorUserId,
        ]);

        if (!$ok) {
            return ['ok' => false, 'errors' => ['general' => t('Could not save the security check')]];
        }

        if ($anyStepGated) {
            return [
                'ok' => true,
                'errors' => [],
                'warning' => t('Saved. Fill in all required fields to complete the affected steps.'),
            ];
        }

        return ['ok' => true, 'errors' => []];
    }

    /**
     * @return array{ok: bool, errors: array<string, string>}
     */
    public function setArchived(int $tenantId, int $id, bool $archived, int $actorUserId): array
    {
        $row = $this->repository->findById($tenantId, $id);
        if ($row === null) {
            return ['ok' => false, 'errors' => ['general' => t('Security check not found')]];
        }

        if ($archived) {
            $status = SecurityCheckProcess::STATUS_ARCHIVED;
        } else {
            $progress = $this->process->computeProgress(
                $this->decode($row['process_state_json'] ?? ''),
                $this->decode($row['tech_schema_snapshot_json'] ?? ''),
                $this->decode($row['tech_state_json'] ?? ''),
                false
            );
            $status = $progress['status'];
        }

        $ok = $this->repository->updateStatus($tenantId, $id, $status, $actorUserId);

        return $ok ? ['ok' => true, 'errors' => []] : ['ok' => false, 'errors' => ['general' => t('Could not update the status')]];
    }

    public function delete(int $tenantId, int $id): bool
    {
        return $this->repository->deleteById($tenantId, $id);
    }

    /**
     * @api Called from checks-data endpoint
     * @param array<string, mixed> $filters
     * @return array{total: int, rows: list<array<string, mixed>>}
     */
    public function listPaged(int $tenantId, array $filters): array
    {
        return $this->repository->listPaged($tenantId, $filters);
    }

    /**
     * Tenant users that can be assigned as the owner of a check.
     *
     * @api Called from the create wizard
     * @return list<array{id: int, display_name: string}>
     */
    public function listAssignableUsers(int $tenantId): array
    {
        return $this->repository->listTenantUsers($tenantId);
    }

    /**
     * @param array<string, mixed> $entry
     * @return array<string, mixed>
     */
    private function applyCompletion(array $entry, bool $done, int $actorUserId, string $now): array
    {
        $wasDone = !empty($entry['done']);
        if ($done && !$wasDone) {
            $entry['done'] = true;
            $entry['by'] = $actorUserId;
            $entry['at'] = $now;
        } elseif (!$done) {
            $entry['done'] = false;
            unset($entry['by'], $entry['at']);
        }
        // done && wasDone → keep existing by/at (completion log preserved)

        return $entry;
    }

    /**
     * @return array<string, mixed>
     */
    private function decode(mixed $json): array
    {
        $decoded = json_decode((string) $json, true);

        return is_array($decoded) ? $decoded : [];
    }
}
