<?php

namespace App\Actions\Communications;

use App\Models\Client\Client;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

class ResolveCallerCommunicableAction
{
    public function handle(int $solicitorId, string $fromNumber): ?Model
    {
        $normalized = $this->normalizePhoneNumber($fromNumber);

        if (blank($normalized)) {
            return null;
        }

        foreach ($this->modelSearchMap() as $modelClass => $columns) {
            $match = $this->findMatch(
                modelClass: $modelClass,
                solicitorId: $solicitorId,
                normalized: $normalized,
                raw: $fromNumber,
                columns: $columns,
            );

            if ($match) {
                return $match;
            }
        }

        return null;
    }

    /**
     * Ordered by match priority.
     *
     * @return array<class-string<Model>, array<int, string>>
     */
    protected function modelSearchMap(): array
    {
        return [
            Client::class => [
                'phone_number',
            ],
        ];
    }

    /**
     * @param  class-string<Model>  $modelClass
     * @param  array<int, string>  $columns
     */
    protected function findMatch(
        string $modelClass,
        int $solicitorId,
        string $normalized,
        string $raw,
        array $columns
    ): ?Model {
        /** @var Model $instance */
        $instance = new $modelClass;

        $table = $instance->getTable();

        $availableColumns = array_values(array_filter(
            $columns,
            fn (string $column) => $this->hasColumn($table, $column)
        ));

        if ($availableColumns === []) {
            return null;
        }

        $query = $modelClass::query();

        if ($this->hasColumn($table, 'solicitor_id')) {
            $query->where('solicitor_id', $solicitorId);
        }

        $normalizedColumns = array_values(array_filter(
            $availableColumns,
            fn (string $column) => Str::contains($column, 'normalized')
        ));

        $rawColumns = array_values(array_filter(
            $availableColumns,
            fn (string $column) => ! Str::contains($column, 'normalized')
        ));

        if ($normalizedColumns !== []) {
            $normalizedMatch = (clone $query)
                ->where(function ($query) use ($normalizedColumns, $normalized) {
                    foreach ($normalizedColumns as $index => $column) {
                        if ($index === 0) {
                            $query->where($column, $normalized);
                        } else {
                            $query->orWhere($column, $normalized);
                        }
                    }
                })
                ->first();

            if ($normalizedMatch) {
                return $normalizedMatch;
            }
        }

        if ($rawColumns !== []) {
            $rawCandidates = $this->rawPhoneCandidates($raw, $normalized);

            foreach ($rawCandidates as $candidate) {
                $rawMatch = (clone $query)
                    ->where(function ($query) use ($rawColumns, $candidate) {
                        foreach ($rawColumns as $index => $column) {
                            if ($index === 0) {
                                $query->where($column, $candidate);
                            } else {
                                $query->orWhere($column, $candidate);
                            }
                        }
                    })
                    ->first();

                if ($rawMatch) {
                    return $rawMatch;
                }
            }
        }

        return null;
    }

    protected function hasColumn(string $table, string $column): bool
    {
        static $cache = [];

        if (! isset($cache[$table])) {
            $cache[$table] = Schema::getColumnListing($table);
        }

        return in_array($column, $cache[$table], true);
    }

    protected function normalizePhoneNumber(?string $value): string
    {
        $value = trim((string) $value);

        if ($value === '') {
            return '';
        }

        $value = preg_replace('/[^\d+]/', '', $value) ?? '';

        if (Str::startsWith($value, '00')) {
            $value = '+' . substr($value, 2);
        }

        if (! Str::startsWith($value, '+')) {
            if (Str::startsWith($value, '0')) {
                $value = '+44' . substr($value, 1);
            } else {
                $value = '+' . $value;
            }
        }

        return $value;
    }

    /**
     * @return array<int, string>
     */
    protected function rawPhoneCandidates(string $raw, string $normalized): array
    {
        $raw = trim($raw);

        $digitsOnly = preg_replace('/\D+/', '', $raw) ?? '';
        $normalizedDigits = preg_replace('/\D+/', '', $normalized) ?? '';

        $candidates = array_filter([
            $raw,
            $normalized,
            $digitsOnly,
            $normalizedDigits,
            Str::startsWith($normalized, '+44') ? '0' . substr($normalized, 3) : null,
            Str::startsWith($normalizedDigits, '44') ? '0' . substr($normalizedDigits, 2) : null,
        ]);

        return array_values(array_unique($candidates));
    }
}