<?php

declare(strict_types=1);

namespace App\Framework\Quality\PHPStan\Rules\Naming;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
 * Enforces Exception Naming Convention:
 * - Domain Exceptions: NO "Exception" suffix (UserNotFound, PaymentFailed)
 * - Technical Exceptions: WITH "Exception" suffix (DatabaseException, ValidationException)
 *
 * @implements Rule<InClassNode>
 */
final class ExceptionNamingRule implements Rule
{
    private const TECHNICAL_NAMESPACES = [
        'Framework',
        'Infrastructure',
    ];

    private const DOMAIN_NAMESPACES = [
        'Domain',
        'Application',
    ];

    public function getNodeType(): string
    {
        return InClassNode::class;
    }

    /**
     * @param InClassNode $node
     */
    public function processNode(Node $node, Scope $scope): array
    {
        $classReflection = $node->getClassReflection();
        $className = $classReflection->getName();
        $shortName = $classReflection->getNativeReflection()->getShortName();

        // Check if class extends Exception or FrameworkException
        if (! $classReflection->isSubclassOf(\Exception::class) &&
            ! $classReflection->isSubclassOf('App\\Framework\\Exception\\FrameworkException')) {
            return [];
        }

        // Skip base FrameworkException
        if ($className === 'App\\Framework\\Exception\\FrameworkException') {
            return [];
        }

        $errors = [];
        $isTechnicalException = $this->isTechnicalException($className);
        $isDomainException = $this->isDomainException($className);

        if ($isTechnicalException) {
            // Technical exceptions MUST have "Exception" suffix
            if (! str_ends_with($shortName, 'Exception')) {
                $errors[] = RuleErrorBuilder::message(sprintf(
                    'Technical exception "%s" must have "Exception" suffix. Rename to "%sException".',
                    $shortName,
                    $shortName
                ))
                ->identifier('naming.exception.technicalSuffix')
                ->build();
            }
        } elseif ($isDomainException) {
            // Domain exceptions MUST NOT have "Exception" suffix
            if (str_ends_with($shortName, 'Exception')) {
                $suggestedName = $this->suggestDomainExceptionName($shortName);
                $errors[] = RuleErrorBuilder::message(sprintf(
                    'Domain exception "%s" should not have "Exception" suffix. Consider renaming to "%s".',
                    $shortName,
                    $suggestedName
                ))
                ->identifier('naming.exception.domainNoSuffix')
                ->tip('Domain exceptions are more expressive without "Exception" suffix: UserNotFound::byId($id)')
                ->build();
            }
        }

        return $errors;
    }

    private function isTechnicalException(string $className): bool
    {
        foreach (self::TECHNICAL_NAMESPACES as $namespace) {
            if (str_contains($className, "\\{$namespace}\\")) {
                return true;
            }
        }

        return false;
    }

    private function isDomainException(string $className): bool
    {
        foreach (self::DOMAIN_NAMESPACES as $namespace) {
            if (str_contains($className, "\\{$namespace}\\")) {
                return true;
            }
        }

        return false;
    }

    private function suggestDomainExceptionName(string $currentName): string
    {
        // Remove "Exception" suffix
        $baseName = preg_replace('/Exception$/', '', $currentName);

        // Convert to domain-friendly name
        // UserNotFoundException -> UserNotFound
        // OrderCancelledException -> OrderCancelled
        // PaymentFailedException -> PaymentFailed

        return $baseName;
    }
}
