<?php

namespace App\Models;

use App\Helpers\StringHelper;
use App\Models\Language;
use App\Models\Locations\Location;
use App\Models\Post;
use App\Models\User;
use App\Notifications\VerifyProfileEmail;
use App\Traits\ActiveStatesTrait;
use App\Traits\HasPresence;
use App\Traits\LocationTrait;
use App\Traits\ProfileTrait;
use App\Traits\TaggableWithLocale;
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
use Cog\Contracts\Love\Reacterable\Models\Reacterable as ReacterableInterface;
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable;
use Illuminate\Auth\MustVerifyEmail as AuthMustVerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Scout\Searchable;
use Namu\WireChat\Traits\Chatable;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Traits\HasRoles;

class Organization extends Authenticatable implements ReacterableInterface, MustVerifyEmail, ReactableInterface
{
    use HasFactory;
    use AuthMustVerifyEmail;
    use Notifiable;
    use HasProfilePhoto;
    use HasRoles;
    use TaggableWithLocale;
    use Reacterable; // cybercog/laravel-love
    use Reactable; // cybercog/laravel-love
    use Searchable; // laravel/scout with ElasticSearch
    use LocationTrait;
    use Chatable;
    use ActiveStatesTrait;
    use LogsActivity; // Spatie Activity Log
    use HasPresence;
    use ProfileTrait;



    /*
     * The attributes that should be hidden for serialization.
     * WARNING: API'S CAN POTENTIALLY EXPOSE ALL VISIBLE FIELDS
     *
     * @var array
     */
    protected $hidden = [
        'email',
        'email_verified_at',
        'full_name',
        'password',
        'remember_token',
        'phone',
        'cyclos_id',
        'cyclos_salt',
        'cyclos_skills',
        'two_factor_confirmed_at',
        'two_factor_recovery_codes',
        'two_factor_secret',
        'limit_min',
        'limit_max',
        'lang_preference',
        'created_at',
        'updated_at',
        'last_login_at',
        'inactive_at',
        'deleted_at',
    ];



    /**
     * The attributes that are mass assignable.
     *
     * @var string[]
     */
    protected $fillable = [
        'name',
        'full_name',
        'email',
        'profile_photo_path',
        'about',
        'about_short',
        'motivation',
        'website',
        'phone',
        'phone_public',
        'password',
        'limit_min',
        'limit_max',
        'lang_preference',
        'last_login_at',
        'last_login_ip'
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'phone_public' => 'boolean',
        'date_of_birth' => 'date',
        'email_verified_at' => 'datetime',
        'inactive_at' => 'datetime',
        'deleted_at' => 'datetime',
    ];

    // protected $guard_name = 'web';

    /**
     * Get the index name for the model.
     *
     * @return string
     */
    public function searchableAs()
    {
        return 'organizations_index';
    }


    /**
    * Convert this model to a searchable array.
    *
    * @return array
    */
    public function toSearchableArray()
    {
        // Prepare eager loaded relationships
        $this->load(
            'languages',
            'locations.district.translations',
            'locations.city.translations',
            'locations.division.translations',
            'locations.country.translations',
            'tags.contexts.tags',
            'tags.contexts.tags.locale',
            'tags.contexts.category.ancestorsAndSelf',
        );

        return [

            'id' => $this->id,
            'name' => $this->name,

            //TODO: Update to multilang database structure in future
            'about_nl' => $this->about,
            'about_en' => $this->about,
            'about_fr' => $this->about,
            'about_de' => $this->about,
            'about_es' => $this->about,
            'about_short_nl' => $this->about_short,
            'about_short_en' => $this->about_short,
            'about_short_fr' => $this->about_short,
            'about_short_de' => $this->about_short,
            'about_short_es' => $this->about_short,
            'motivation_nl' => $this->motivation,
            'motivation_en' => $this->motivation,
            'motivation_fr' => $this->motivation,
            'motivation_de' => $this->motivation,
            'motivation_es' => $this->motivation,
            'cyclos_skills' => $this->cyclos_skills,    // Legacy column, will not be used in the future
            'website'       => $this->website,

            'last_login_at' => $this->last_login_at,
            'lang_preference' => $this->lang_preference,

            'languages' => $this->languages->map(function ($language) { // map() as languages is a collection
                return [
                    'id' => $language->id,
                    'name' => $language->name,
                    'lang_code' => $language->lang_code,
                ];
            }),

            'locations' => $this->locations->map(function ($location) { // map() as locations is a collection
                return [
                    'id' => $location->id,
                    'district' => $location->district ? $location->district->translations->map(function ($translation) {   // map() as translations is a collection
                        return $translation->name;
                    })->toArray() : [],
                    'city' => $location->city ? $location->city->translations->map(function ($translation) {   // map() as translations is a collection
                        return $translation->name;
                    })->toArray() : [],
                    'division' => $location->division ? $location->division->translations->map(function ($translation) {   // map() as translations is a collection
                        return $translation->name;
                    })->toArray() : [],
                    'country' => $location->country ? $location->country->translations->map(function ($translation) {   // map() as translations is a collection
                        return $translation->name;
                    })->toArray() : [],
                ];
            }),

            'tags' => $this->tags->map(function ($tag) {
                return [
                    'contexts' => $tag->contexts
                        ->map(function ($context) {
                            return [
                                'tags' => $context->tags->map(function ($tag) {
                                    // Include the locale in the field name for tags
                                    return [
                                        'name_' . $tag->locale->locale => StringHelper::DutchTitleCase($tag->normalized),
                                    ];
                                }),
                                'categories' => Category::with(['translations' => function ($query) { $query->select('category_id', 'locale', 'name');}])
                                    ->find([ $context->category->ancestorsAndSelf()->get()->flatMap(function ($related) {
                                        $categoryPath = explode('.', $related->path);
                                        return $categoryPath;
                                    })
                                    ->unique()->values()->toArray()
                                    ])->map(function ($category) {
                                        // Include the locale in the field name for categories
                                        return $category->translations->mapWithKeys(function ($translation) {
                                            return ['name_' . $translation->locale => StringHelper::DutchTitleCase($translation->name)];
                                        });
                                    }),
                            ];
                        }),
                ];
            })
        ];
    }


    /**
     * Get the organization's profile.
     * One-to-one
     */
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }


    /**
     * Get all of the organization's accounts.
     * One-to-many polymorphic.
     *
     * @return void
     */
    public function accounts()
    {
        return $this->morphMany(Account::class, 'accountable');
    }


    /**
     * Check if the organization has any associated accounts.
     *
     * @return bool Returns true if the user has accounts, false otherwise.
     */
    public function hasAccounts()
    {
        $accountsExists = DB::table('accounts')
                            ->where('accountable_id', $this->id)
                            ->where('accountable_type', 'App\Models\User')
                            ->exists();
        return $accountsExists;
    }


    /**
     * Get the profile's bank(s) that it can manage.
     * Many-to-many.
     */
    public function banksManaged()
    {
        return $this->belongsToMany(Bank::class);
    }


    public function banksClient()
    {
        return $this->morphToMany(Bank::class, 'client', 'bank_clients')
            ->wherePivot('relationship_type', 'local');
    }


    public function attachBankClient($bank, $relationshipType = null)
    {
        // Set default relationship type if not provided
        $relationshipType = $relationshipType ?? 'local';

        // Accept either ID or Bank model
        $bankId = $bank instanceof Bank ? $bank->id : $bank;

        // Verify existence if numeric ID was provided
        if (is_numeric($bankId)) {
            if (!Bank::where('id', $bankId)->exists()) {
                throw new \Exception("Bank with ID {$bankId} does not exist");
            }
        }

        $this->banksClient()->sync([$bankId => [
            'relationship_type' => $relationshipType,
            'created_at' => now(),
            'updated_at' => now()
        ]]);

        return $this;
    }


    public function detachBankClient($bank, $relationshipType = null)
    {
        // Set default relationship type if not provided
        $relationshipType = $relationshipType ?? 'local';

        // Accept either ID or Bank model
        $bankId = $bank instanceof Bank ? $bank->id : $bank;

        // Verify existence if numeric ID was provided
        if (is_numeric($bankId)) {
            if (!Bank::where('id', $bankId)->exists()) {
                throw new \Exception("Bank with ID {$bankId} does not exist");
            }
        }

        // Detach with additional pivot conditions
        $this->banksClient()->wherePivot('relationship_type', $relationshipType)
                    ->detach($bankId);

        return $this;
    }


    /**
     * Get the organization's user(s).
     * Many-to-many.
     */
    // todo: replace instances to managers() as these are actually profile managers and no organization users. See $this-> manangers()
    public function users()
    {
        return $this->belongsToMany(User::class);
    }


    /**
     * Get the organization's profile manager(s).
     * Many-to-many.
     */
    public function managers()
    {
        return $this->belongsToMany(User::class);
    }


    /**
     * Get all of the languages for the organization.
     * Many-to-many polymorphic.
     */
    public function languages()
    {
        return $this->morphToMany(Language::class, 'languagable')->withPivot('competence');
    }


    /**
     * Get all of the social for the organization.
     * Many-to-many polymorphic.
     */
    public function socials()
    {
        return $this->morphToMany(Social::class, 'sociable')->withPivot('id', 'user_on_social', 'server_of_social');
    }


    /**
     * Get all related the locations of the organization.
     * One-to-many polymorph.
     */
    public function locations()
    {
        return $this->morphMany(Location::class, 'locatable');
    }


    /**
     * Get all of the organization's message settings.
     */
    public function message_settings()
    {
        return $this->morphMany(MessageSetting::class, 'message_settingable');
    }


    /**
     * Get all of the Organization's posts.
     */
    public function posts()
    {
        return $this->morphMany(Post::class, 'postable');
    }


    /**
     * Get all of the Organization's posts.
     */
    public function categories()
    {
        return $this->morphMany(Category::class, 'categoryable');
    }


    public function sendEmailVerificationNotification()
    {
        \Mail::to($this->email)->send(new \App\Mail\VerifyProfileEmailMailable($this));
    }


    /**
     * Configure the activity log options for the User model.
     *
     * This method sets up the logging to:
     * - Only log changes to the 'name', 'password', and 'last_login_ip' attributes.
     * - Log only when these attributes have been modified (dirty).
     * - Prevent submission of empty logs.
     * - Use 'user' as the log name.
     *
     * @return \Spatie\Activitylog\LogOptions
     */
    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly([
                'name',
                'last_login_ip',
                'inactive_at',
                'deleted_at',
            ])
            ->logOnlyDirty()   // Only log attributes that have been changed
            ->dontSubmitEmptyLogs()
            ->useLogName('Organization');
    }


    /**
    * Wirechat: Returns the URL for the user's cover image (avatar).
    * Adjust the 'avatar_url' field to your database setup.
    */
    public function getCoverUrlAttribute(): ?string
    {
        return Storage::url($this->profile_photo_path) ?? null;
    }


    /**
    * Wirechat:Returns the URL for the user's profile page.
    * Adjust the 'profile' route as needed for your setup.
    */
    public function getProfileUrlAttribute(): ?string
    {
        return route('profile.show_by_type_and_id', ['type' => __('organization'), 'id' => $this->id]);
    }


    /**
    * Wirechat: Returns the display name for the user.
    * Modify this to use your preferred name field.
    */
    public function getDisplayNameAttribute(): ?string
    {
        if ($this->full_name !== $this->name) {
            return $this->full_name . ' (' . $this->name . ')';
        } elseif ($this->name) {
            return $this->name;
        } else {
            return __('Organization');
        }
    }


    /**
    * Wirechat: Search for users when creating a new chat or adding members to a group.
    * Customize the search logic to limit results, such as restricting to friends or eligible users only.
    */
    public function searchChatables(string $query): ?\Illuminate\Support\Collection
    {
        $searchableFields = ['name', 'full_name'];

        // Search across all profile types
        $users = \App\Models\User::where(function ($queryBuilder) use ($searchableFields, $query) {
            foreach ($searchableFields as $field) {
                $queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
            }
        })->limit(6)->get();

        $organizations = Organization::where(function ($queryBuilder) use ($searchableFields, $query) {
            foreach ($searchableFields as $field) {
                $queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
            }
        })->limit(6)->get();

        $banks = \App\Models\Bank::where(function ($queryBuilder) use ($searchableFields, $query) {
            foreach ($searchableFields as $field) {
                $queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
            }
        })->limit(6)->get();

        $admins = \App\Models\Admin::where(function ($queryBuilder) use ($searchableFields, $query) {
            foreach ($searchableFields as $field) {
                $queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
            }
        })->limit(6)->get();

        // Combine all results into a base Collection to avoid serialization issues
        $results = collect()
            ->merge($users->all())
            ->merge($organizations->all())
            ->merge($banks->all())
            ->merge($admins->all());

        // Filter out profiles based on configuration
        return $results->filter(function ($profile) {
            // Check inactive profiles
            if (timebank_config('profile_inactive.messenger_hidden') && !$profile->isActive()) {
                return false;
            }

            // Check email unverified profiles
            if (timebank_config('profile_email_unverified.messenger_hidden') && !$profile->isEmailVerified()) {
                return false;
            }

            // Check incomplete profiles
            if (
                timebank_config('profile_incomplete.messenger_hidden')
                && method_exists($profile, 'hasIncompleteProfile')
                && $profile->hasIncompleteProfile($profile)
            ) {
                return false;
            }

            return true;
        })->take(6)->values();
    }


    /**
    * Wirechat: Determine if the user can create new groups.
    */
    public function canCreateGroups(): bool
    {
        return  timebank_config('profiles.organization.messenger_can_create_groups');
    }


    /**
     * Check if the Organization has any transactions with another model (User, Organization, Bank).
     *
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return bool
     */
    public function hasTransactionsWith($model): bool
    {
        $modelClass = get_class($model);
        $modelId = $model->id;

        // Check if this Organization is involved in any transaction where the other model is either the sender or receiver
        return Transaction::where(function ($query) use ($modelClass, $modelId) {
            $query->whereHas('accountFrom.accountable', function ($q) use ($modelClass, $modelId) {
                $q->where('accountable_type', $modelClass)
                    ->where('accountable_id', $modelId);
            })
                ->orWhereHas('accountTo.accountable', function ($q) use ($modelClass, $modelId) {
                    $q->where('accountable_type', $modelClass)
                        ->where('accountable_id', $modelId);
                });
        })
            ->where(function ($query) {
                $query->whereHas('accountFrom.accountable', function ($q) {
                    $q->where('accountable_type', Organization::class)
                        ->where('accountable_id', $this->id);
                })
                    ->orWhereHas('accountTo.accountable', function ($q) {
                        $q->where('accountable_type', Organization::class)
                            ->where('accountable_id', $this->id);
                    });
            })
            ->exists();
    }

    /**
     * Get the message settings for this organization
     */
    public function messageSettings()
    {
        return $this->morphMany(MessageSetting::class, 'message_settingable');
    }
}
