<?php

namespace CouponsPlus\App\Coupon\Tree\Factories;

use Closure;
use CouponsPlus\App\Conditions\Card;
use CouponsPlus\App\Coupon\CardData;
use CouponsPlus\App\Coupon\TestableData;
use CouponsPlus\App\Coupon\Tree\Abilities;
use CouponsPlus\App\Coupon\Tree\Branch;
use CouponsPlus\App\Coupon\Tree\GroveData;
use CouponsPlus\App\Coupon\Tree\NodeData;
use CouponsPlus\App\Coupon\Tree\TestableGroupData;
use CouponsPlus\App\Offers\Offer;
use CouponsPlus\Original\Collections\Collection;
use Error;
use stdClass;
use CouponsPlus\App\Coupon\Tree\Data\Transformers\UnchangedTransformer;
use CouponsPlus\App\Coupon\Tree\Data\Transformers\Abilities\DataTransformer;

/**
 * This should definitely be separated into several classes, each of a component type (one factory for nodes, branches, testablegroups, etc) and each should have its own tests
 * 
 * But for now this will have to suffice, sorry, i dont like it but anit nobody got time for dat rn.
 */
class DataStructureFromJSONFactory
{
    /**
     * Version 0 is what we used in the beginning, after all tests have been written and before building the dashboard (client))
     * Version 1 is the good one, currently it mainly uses node.testable instead of node.testableGroup of version 0
     */
    protected int $version = 0;
    protected DataTransformer $testableTransformer;
    private TestableGroupDataFromStandardObjectFactory $testableGroupDataFromStandardObjectFactory;

    public function __construct(TestableGroupDataFromStandardObjectFactory $testableGroupDataFromStandardObjectFactory, ?DataTransformer $testableTransformer = null)
    {
        $this->testableGroupDataFromStandardObjectFactory = $testableGroupDataFromStandardObjectFactory;
        $this->testableTransformer = $testableTransformer ?? new UnchangedTransformer;
    }

    /**
     * Throws exception on failure
     *
     * We're NOT catching and returning the exception here because:
     * 
     * 1. It's very hard to test when something goes wrong (it would return an empty collection on error)
     * 2. On production we can have this method be called inside a try catch block, externally. I'm thinking maybe even inside it's own decorator
     */
    public function createFromSource(string $groveAsJSONString, ?int $version = null) : GroveData
    {
        (object) $grove = new Collection([]);

        try {
            (object) $rootNodesRawData = json_decode($groveAsJSONString);

            /**
             * Ideally this should be its own class but im gonna leave it like this rn to make it as short as possible then i might refactor....
             */
            foreach ($rootNodesRawData as $rootNodeRawData) {
                $grove->push($this->createNodeFromData($rootNodeRawData, $version));
            }
        } catch (Error $exception) {
            throw $exception;
            //return $grove;
        }

        return new GroveData(...$grove->asArray());
    }

    public function createNodeFromData(stdClass $rootNodeRawData, ?int $version = null) : Abilities\NodeData
    {
        if (!is_null($version)) {
            $this->version = $version;
        }

        return new NodeData(
            $this->createTestableGroupFromData(
                $this->version === 0 ? $rootNodeRawData->testableGroup : $rootNodeRawData->testable
            ),
            $this->createBranchFromData($rootNodeRawData->branch)
        );
    }

    protected function createBranchFromData(stdClass $branchRawData) : Abilities\Branch
    {
        (array) $children = [];

        switch($branchRawData->type) {
            case 'offers':
                $children = $this->createOffersFromData($branchRawData->children);
            break;
            default:
            /*case 'first-passing-node':
            case 'filter-returning-the-most-items':
            case 'bottom-to-top-first-passing-node':*/
                $children = $this->createNodesFromData($branchRawData->children);
            break;
        }

        return new Branch(
            $branchRawData->type,
            $children,
            $branchRawData->data ?? null,
            //Collection::create($branchRawData->children)->map([$this, 'createNode'])->asArray(),
        );
    }

    /**
     * @return Abilities\TestableGroupData|Abilities\TestableData
     */
    public function createTestableGroupFromData(stdClass $testableGroupRawData)
    {
        return $this->testableGroupDataFromStandardObjectFactory->create(
            $this->testableTransformer->transform($testableGroupRawData)
        );
    }

    protected function createOffersFromData(array $offersRawData) : array
    {
        return Collection::create($offersRawData)->map(function(stdClass $offerRawData) : CardData {
            return new CardData($offerRawData->type, Offer::createOptions($offerRawData), $offerRawData->id ?? null);
        })->asArray();
        /*
        return Collection::create($offersRawData)->map(function(stdClass $offerRawData) : Offer {
            return Offer::createFromOptions($offerRawData, $this->coupon);
        })->asArray();
        */
    }

    protected function createNodesFromData(array $nodes) : array
    {
        return Collection::create($nodes)->map([$this, 'createNodeFromData'])->asArray();
    }

    //todo: create tesable group
    //      create the remaining branches
    //      
    //      create the runtime offer finders from this structure
}