<?php

namespace Clevercherry\Backslash4\Traits;

use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Illuminate\Pagination\LengthAwarePaginator;
use App\Models\Block; //has to be the app version so that functions are overridable
use App\Models\MediaManager;
use Clevercherry\Backslash4\Backslash4;
use Clevercherry\Backslash4\Models\breadCrumb;
use Clevercherry\Backslash4\Models\Model;
use Clevercherry\Backslash4\Traits\searchModel;
use Illuminate\Http\Request;
use stdClass;

trait baseModel
{
    use searchModel;
    use metaTrait;

    public function __construct()
    {   
        // if(Schema::hasColumn($this->getTable(),'active'))
        //     $this->active = Model::STATE_ACTIVE;

        parent::__construct();
    }

    /**
     * Returns an array of model relationships, defaults to an empty array
     * 
     * @return array
     */
    public function relationships() : array
    {
        return [];
    }

    /**
     * Return the string algorithm used for the ?preview=HASH URLs for inactive model $items
     * @return bool | string     false or the empty string do not allow previews of the model items on the front end
     *                           if it returns a string "eg. "SHA1" that is the algorithm used to generate the value (from the slug)
     *                           for the suffix ?preview=HASH
     *                           true is NOT a valid value to return
     *                           valid values per https://www.php.net/manual/en/function.hash-algos.php
     */
    public static function allowPreviewAlgorithm() : bool | string
    {
        return false;
    }

    /**
     * Returns a collection instance paginated to $pageSize, default 10 results
     * 
     * @return Illuminate\Pagination\LengthAwarePaginator
     */
    public static function adminListing($pageSize=10) : LengthAwarePaginator
    {
        return self::adminOrder()
            ->indexSearch(request()->q)
            ->paginate($pageSize)
            ->appends(array_merge(request()->query(), request()->post()));
    }
    /**
     * Returns a collection instance paginated to $pageSize, default 10 results
     * 
     * @return Illuminate\Pagination\LengthAwarePaginator
     */
    public static function adminListingWith($pageSize=10, $queryTerms=[]) : LengthAwarePaginator
    {
        return self::adminOrder()
            ->indexSearchWith($queryTerms)
            ->paginate($pageSize)
            ->appends(array_merge(request()->query(), request()->post()));
    }

    /**
     * Indicates whether the model supports content blocks.
     *
     * @return bool True if the model uses block-based content; otherwise false.
     */
    public function hasBlocks()
    {
        return true;
    }

    public static function canAddBlocks()
    {
        return true;
    }

    public static function showBlockActions()
    {
        return true;
    }

    public static function canMoveBlocks()
    {
        return true;
    }
    
    /**
     * Determines if the search cache should run or not for the given model
     * 
     * @return bool
     */
    public function searchCache() : bool
    {
        return false;
    }

    public function getBlocks($active=null)
    {
        if($active)
            return $this->getActiveBlocks();

        //else
        return $this->getAllBlocks();
        
    }

    /**
     * Override if shown on CMS Dashboard Menu
     */
    public static function showOnDashboardMenu() : bool
    {
        return true;
    }

    public function getDownloadUrl(string $field_name) : string
    {
        return route('admin.' . $this->getTable() . '.download', ['id' => $this->id, 'field_name' => $field_name]);
    }

    /**
     * Turn a path from MediaManager or stored in a files field into a full url that can be downloaded
     * @param  String $path     a full url, or media manager storage path
     * @return String           the full url to download the file even if private
     */
    public static function getFileDownloadUrl($path)
    {
        if(Str::startsWith($path,['http:','https:']))
            return $path;
        
        if(empty($path))
            return '';

        return route('admin.dashboard.downloadpath', ['file_path' => $path ]);
    }

    /**
     * return the url path and slug
     */
    public function getUrl() : String
    {
        $menuSlug = $this->getFrontEndMenuSlug();
        if(empty($menuSlug))
            $url = '/'.$this->slug;
        else
            $url = '/'.$menuSlug.'/'.$this->slug;

        // if no algorithm set (default) just return the URL
        $algo = self::allowPreviewAlgorithm();
        if(empty($algo))
            return $url;

        // if the item is active just return the plain url
        if(Schema::hasColumn($this->getTable(),'active') && ($this->active == Model::STATE_ACTIVE))
            return $url;

        //so we have an algorithm and it's inactive so return the preview URL
        return $url . '?preview=' . hash($algo, $this->slug);
    }

    /**
     * get the url of an item by Id
     */
    public static function getUrlById($id)
    {
        return @self::where('id', $id)->first()->getUrl();
    }

    /**
     * Provide hard coded routing instructions for inclusion in the bs4 web.php
     * and return a boolean whether the auto dashboard routing should be run
     * 
     * @return bool : flag whether the front end automatic routing should be run
     */
    public static function provideRoutingAndFlag() : bool
    { 
        //run no extra rounting and indicate that the automatic routing in bs4 web.php shall run
        return true; 
    }

    /**
     * Returns the frontend menu slug for this model/module.
     *
     * The slug is resolved from the Backslash4 dashboard configuration
     * using the module's CMS configuration entry.
     *
     * Resolution logic:
     * - Defaults to the module's configured CMS menu slug
     * - Uses "fe-slug" override if defined in configuration
     * - Returns an empty slug for the "pages" module
     *
     * Optionally prepends a leading slash for URL usage.
     *
     * @param bool $prefixSlash Whether to prefix the returned slug with "/".
     * 
     * @return string The resolved frontend menu slug.
     */
    public function getFrontEndMenuSlug($prefixSlash=false) : String
    {
        //$item = Backslash4::getDashboardConfig()['cmsMenu'][$this->getCMSModuleName()];
        $item = Backslash4::getDashboardFlatCMS()[$this->getCMSModuleName()];
        
        $module = strtolower(Str::plural($this->getTable()));
        if($module=='pages')
          $menuSlug='';
        else
          $menuSlug = $item['slug'];
        
        if(isset($item['fe-slug']) && !empty($item['fe-slug']))
          $menuSlug = $item['fe-slug'];

        return ($prefixSlash? '/' : '').$menuSlug;
    }

   /**
    * return an string for the route for this model, or the default provided
    * you'll want to modify the two functions to match
    *   public function getUrl() : String
    *   public function getFrontEndMenuSlug($prefixSlash=false) : String
    * You'll also have to override the view in the view($slug) in the frontend Controller
    *
    * @param  String   $default       the route to return if not overridden (usually retreived from the backslash4 config file)
    * 
    * @return String                  the route for use in web.php excluding the trailing /{slug}, as it is also used for the home/index view for the model
    */
    public static function getFrontEndRoute($default, $is_index=false) : String | null
    {
        return $default;
    }

    /**
     * Get the name of the index module name used in the backslash4 config Backslash4::getDashboardConfig()['cmsMenu'][$module]
     * derived from the Table name (with retr exception of the Mediamanegr which is not plural
     * 
     * @return String   the name of the index module name used in the backslash4 config Backslash4::getDashboardConfig()['cmsMenu'][$module]
     */
    public function getCMSModuleName()
    {
        $module = Str::studly($this->getTable());
        if($module=='MediaManagers')
            $module = 'MediaManager';
        
        return $module;
    }
    
    /**
     * Get the name of the index module name used in the backslash4 config Backslash4::getDashboardConfig()['cmsMenu'][$module]
     * derived from the Table name (with retr exception of the Mediamanegr which is not plural
     * @return String   the name of the index module name used in the backslash4 config Backslash4::getDashboardConfig()['cmsMenu'][$module]
     */
    public function getCMSModuleDisplayName()
    {
        $config = Backslash4::getDashboardFlatCMS()[$this->getCMSModuleName()];
        
        if(! isset($config['name'])) {
            return $this->getCMSModuleName();
        }

        return $config['name'];
    }

    /**
     * Returns the most appropriate human-readable identifier for the model.
     *
     * Attempts to resolve the name in the following order:
     * - name
     * - title
     * - userName
     * - slug
     * - fallback: table name
     *
     * @return string The best available display name for the model.
     */
    public function getNameTitle() : String
    {
        return @$this->name ?? @$this->title ?? @$this->userName ?? @$this->slug ?? $this->getTable();
    }

    public static function getNamebyId($id) : string
    {
        return self::where('id',$id)->first()->getNameTitle();
    }
    
    /**
     * get a short description limited string length
     */
    public function getShortenedDescription($length=255) : String
    {
        if(Schema::hasColumn($this->getTable(),'description'))
            return self::limitHtml($this->description, $length);
        
        return  Str::limit(strip_tags($this->getNameTitle()), $length, '...');
    }

    /**
     * Alias for getNameTitle()
     * May be overridden in your model for something specific
     * @deprecated
     * @return string
     */
    public function getName() : String
    {
        return $this->getNameTitle();
    }

    /**
     * @deprecated - now uses metaTrait
     */
    public function getSEOName()
    {
        return $this->getMetaTitle();
    }
    

    /**
     * return null or breadCrumb
     * @return ?breadCrumb  null or object containing ->text and ->link, or no parameters
     */
    public function getBreadcrumbsParent() : ?breadCrumb
    {
        if(empty($this->slug) || ($this->slug=='/'))
            return null;
        //else

        return new breadCrumb('Home', '/');
    }

    public static function getIndexBreadcrumbArray($adminPrefix, $slug, $title) : array
    {
        return [ 'text' => Str::headline($title), 
                 'href' =>  $adminPrefix . '/' . $slug ];
    }

    /**
     * Returns an array of breadcrumbs for the CMS, empty by default
     * but can be overwritten at the application level
     * 
     * @return array []
     */
    public function getCmsBreadcrumbs() : array
    {
        try {        
            $return = [];

            $return[$this->getCMSModuleDisplayName()] = route('admin.' . $this->getTable() . '.index');

            if($this->id) {
                $return[$this->getNameTitle()] = route('admin.' . $this->getTable() . '.edit', ['id' => $this->id]);
            } else {
                $return['Create'] = route('admin.' . $this->getTable() . '.create');
            }

            return $return;
        } catch(\Exception $e) {
            return [];
        }
    }

    /**
     * Determines if a given item can be duplicated or not
     * 
     * @return bool
     */
    public function canDuplicate() : bool
    {
        return false;
    }


      /**
     * Determines if a given item has duplication route
     * 
     * @return bool
     */
    public static function hasDuplicateRoute() : bool
    {
        return false;
    }

    public static function allowAPI() : bool
    {
        return false;
    }

    /**
     * Determines if a model implements the standard CMS routing in BS4
     * you can add extra routing in this function, or turn off the default routing and replace it here
     * 
     * @return bool    default true
     */
    public static function hasStandardCMSRouting() : bool
    {
        return true;
    }

    /**
     * return an array of breadcrumbs indexed from 0=home /, to ... n this page
     * thsi should automatically correct for one/two layer navigations in most cases, in which case you just
     * want to use getBreadcrumbsParent()
     * @return Array[breadCrumb] 
     */
    public function getBreadcrumbs() : Array
    {
        $crumbs =[];
        $parent = $this->getBreadcrumbsParent();
        if(!empty($parent) && ($parent->url!="/"))
            $crumbs[] = new breadCrumb('Home', '/');
        
        $crumbs[] = $parent;
        $crumbs[] = new breadCrumb($this->getNameTitle(), $this->getUrl());

        return $crumbs;
    }

    /**
     * @deprecated - now uses metaTrait
     */
    public function getSEODescription()
    {
        return $this->getMetaDescription();
    }

    /**
     * Whether this model/row should be included/skipped when generating the sitemap, generally 
     * dependant upon active or status flags on the record, making this as internal to the dtaa in the 
     * record as possible will make it run faster
     * you may want to test the Request()->slug to see if it sould be included in the particular sitemap pages file
     * @return bool      Whether this model/row should be included skipped when generating the sitemap
     */
    public function includeInSitemap() : bool
    {
        //$slug = @Request->slug ?? '';
        //return in_array($slug, self::getSiteMapPages())
        return false;
    }

    /**
     * Whether the whole model should be included skipped when generating the sitemap,
     * called once per model so that every record can be skipped instead of calling includeInSitemap() on 
     * every record, must return true if any table rows/records are to be queried
     * you may want to test the Request()->slug to see if it sould be included in the particular sitemap pages file
     * @return bool      Whether the whole model should be included skipped when generating the sitemap
     */
    public function includeModelInSitemap() : bool
    {
        //$slug = @Request->slug ?? '';
        //return in_array($slug, self::getSiteMapPages())
        return false;
    }

    /**
     * By defult all items are returned and each inidividual one is queried whether it shoudl be included in the sitemap
     * You can override this function to optimise the liste of items provided to the sitemap and possibly just return
     * true in the includeInSitemap function as the model can already be filtered here
     */
    public function getSiteMapItems()
    {
        return self::all();
    }

    /**
     * Used to define additional URLs that may be hard coded in to the application routes file
     * May be overridden in your model, will default to an empty array
     */
    public function additionalSitemapUrls() : array
    {
        /**
         * Below code is an example of how to define additional
         * URLs that are compatible with the backslash sitemap
         */

        // $array = [];

        // $url = new \stdClass;
        // $url->loc = 'http://example.com';
        // $url->lastmod = '2023-01-01 23:23:59';
        // $url->changefreq = 'daily';
        // $url->priority = '1.0';

        // $array[] = $url;
        
        return [];
    }

    /**
     * return a list of sitemap names that will include items from this model
     * the '' empty string indicates the main single sitemap.xml file
     * @return array      an array of sitemap page names ({slug}s) that will be generated for this model
     *                    '' the empty string (the default) indicates the model will provide entries for the default sitmap.xml
     *                    other entries will be sitemap files in the format sitemap_{slug}.xml
     */
    public static function getSiteMapPages() :array
    {
        return [''];
    }

    public function orderable() : null|string
    {
        return null;
    }

    //overidable routing for the item rows on the index page
    public function getEditUrl($adminRoute)
    {
        return route($adminRoute.'.edit',['id'=>$this->id]);
    }

    public function getDeleteUrl($adminRoute)
    {
        return route($adminRoute.'.delete',['id'=>$this->id]);
    }

    public function getOrderableUrl($adminRoute, $direction)
    {
        return route($adminRoute.'.orderable',['id'=>$this->id, 'direction' => $direction] + Request()->all());
    }

    // static because it's not called on a row/item
    public static function getDataAjaxUrl($adminRoute)
    {
        return route($adminRoute.'.index.ajax');
    }


    /**
     * Alias for includeInSitemap()
     * May be overridden in your model for something specific
     */
    public function robotsIndex() : bool
    {
        return $this->includeInSitemap();
    }

    /**
     * May be overridden in your model for something specific
     */
    public function robotsFollow() : bool
    {
        return true;
    }    
    
    /**
     * get all the block names available for the editor to add
     * so that it can be overrridden on a modal basis
     * you may be better overriding it in the Block model itself and detceting classes on the $item
     * @return Array        String of blck model names
     */
    public function getAddableBlockNames() : Array
    {
        return \App\Models\Block::getBlockNames($this);
    }

    /**
     * return the blocks for this model item
     */
    public function getActiveBlocks()
    {
        if($this->hasBlocks())
        {
            $blocks = Block::where('model', $this->getTable())
                            ->where('modelId', $this->id)
                            ->orderBy('order')
                            ->active()
                            ->get();
            
            if(count($blocks)==0)
            {
                //if no blocks return the required ones
                // which is the same as default unless overridden in the model

                $requiredBlocks = $this->getRequiredBlocks();
                $blocks=[];
                foreach($requiredBlocks AS $key => $defaultBlock) {
                    $newBlock = new Block;
                    $newBlock->id = $defaultBlock . $key;
                    $newBlock->type = $defaultBlock;
                    $newBlock->active = 1;
        
                    $blocks[] = $newBlock;
                }
            }

            return $blocks;
        }
        //else
        return [];
    }

    /**
     * return the all the blocks for this model item
     */
    public function getAllBlocks()
    {
        $blocks=[];

        if(empty($this->id)) //empty id is a create
            $defaultBlocks = $this->getDefaultBlocks();
        else
            $defaultBlocks = $this->getRequiredBlocks();

        if(!empty($this->id) && $this->hasBlocks())
        {
            //empty id is a create so no point trying to get blocks
            
            $blocks = Block::where('model', $this->getTable())
                     ->where('modelId', $this->id)
                     ->orderBy('order')
                     ->get();
        }

        foreach($blocks AS $block) {
            if (($key = array_search($block->type, $defaultBlocks)) !== false) {
                unset($defaultBlocks[$key]);
            }
        }

        foreach($defaultBlocks AS $key => $defaultBlock) {
            $newBlock = new Block;
            $newBlock->id = $defaultBlock . $key;
            $newBlock->type = $defaultBlock;
            $newBlock->active = 1;

            $blocks[] = $newBlock;
        }
        
        return $blocks;
    }

    /**
     * If no blocks get a list of the default blocks
     * 
     * @return array[string]     block names required
     */
    public function getDefaultBlocks()
    {
        return [];
    }

    /**
     * When saving a model these are the blocks that must exist, and will be added back in if you deleted them
     * by default they are the same as getDefaultBlocks()
     * override with a null aray to only apply the default b locks when creating a new model
     * 
     * @return array[string]     block names required
     */
    public function getRequiredBlocks() : array
    {
        return (array) $this->getDefaultBlocks();
    }

    /**
     * return media for a carousel
     * @param  String $field_name  name of a field containing media json
     * @return Array  Media items
     */
    public function getMedia($field_name)
    {
        return MediaManager::getMediaItemsFromJson($this->$field_name);
    }

    /**
     * return media for a carousel
     * @param  String $field_name  name of a field containing media json
     * @return Array  Media items
     */
    public function getFirstMedia($field_name)
    {
        return MediaManager::getFirstMediaItemFromJson($this->$field_name);
    }

    /**
     * return a human readable version of the field name, override this function in the specific model
     * to provide client friendly names
     */
    public function translateFieldName($field_name)
    {
        switch ($field_name) {
            case 'email':
                return 'E-mail';
                break;
            case 'seo_title':
                return 'Meta title';
                break;
            case 'seo_name':
                return 'Meta name';
                break;
            case 'seo_description':
                return 'Meta description';
                break;
            case 'sku':
            case 'uom':
                return Str::upper($field_name);
                break;
            case 'active':
                return Str::headline(Str::Singular($this->getTable())) . ' active';
                break;
        }
        
        return Str::headline($field_name);
    }

    /**
     * @return String placeholder text for the input field default empty string
     */
    public function getPlaceholderText($field_name)
    {
        return '';
    }

    /**
     * returns string to hint at the usage description of the field
     * @param  String $field_name  the field being queried for information
     * @return String              the text to be used a s a hint/description for this field
     */
    public function getHintText($field_name) : String
    {        
        if(in_array($field_name, ['active']))
            return 'If unchecked, this '.Str::Singular($this->getTable()).' won\'t be visible to the public.';
        
        return '';
    }


    /**
     * return a list of valid pick from parents -- override this function in your specific model
     * should return an index array[id]="name" for use in a select statement
     * return null      no parents
     */
    public function getChooseParents()
    {
        return null;
    }

    /**
     * return list of this model items that are children of thsi item
     * @return Iterable
     */
    public function getChildren() : Iterable
    {
        if(Schema::hasColumn($this->getTable(),'parent'))
            return self::where('parent', $this->id)->get();

        return [];
    }

    /**
     * whether the index page shows children under the parent items
     * the individual model should filter out the children in the first list of items to the index page
     * see getAdminListing() and scopeSearchIndex in the page model for an example
     */
    public static function showChildren()
    {
        return false;
    }

    /**
     * Allow the model to indent rows by it's own rules
     */
    public function getRowIndent($indent=0) {
        return $indent;
    }

    /**
     * get all other items of this model excepting myself as an [id] index array of names for a select box
     */
    public function getAllOthers($active=true)
    {
        $list = [];
        if($active && Schema::hasColumn($this->getTable(),'active'))
            $items = self::active()->where('id', "!=", $this->id)->get();
        else
            $items = self::where('id', "!=", $this->id)->get();
    
        foreach($items as $item)
            $list[$item->id] = $item->getNameTitle();

        return $list;
    }
    /**
     * 
     */
    public function getFieldTags($field_name, $active=true)
    {
        return [];
    }

    /**
     * Return a csv string of fields that the image show have
     * @param  String $field_name   field name in the model
     * @return String               a csv string of field names, should alway include alt
     */
    public function getFieldMediaFields($field_name): string
    {
        return 'alt';
    }

    /**
     * get a list for a select box based upon field_name
     * see product model for an example override
     */
    public function getChooseOptions($field_name)
    {
        // see product model for an example override
        return [];
    }

    /**
     * get a list for a input/datalist box based upon field_name
     */
    public function getDataList($field_name)
    {
        // see product model for an example override
        return [];
    }

    /**
     * Determines whether a given field should be marked as required in the CMS editor.
     *
     * By default, fields are treated as required unless explicitly excluded.
     * This method allows both field name and field type rules to override that behavior.
     *
     * A field is NOT required if:
     * - It is a known optional system field (e.g. slug, active, timestamps, related)
     * - It uses a non-required input type (e.g. checkboxes, switches, file pickers, parent selectors)
     * - It is part of SEO or meta data fields (prefixed with seo_ or meta_)
     *
     * Models or application-specific implementations may override this method
     * to customise validation rules.
     *
     * @param string $field_name The field name being evaluated.
     * 
     * @return string Returns 'required' if the field is required, or an empty string if optional.
     */
    public function getFieldRequired($field_name)
    {
        if(in_array($field_name, ['slug', 'active', 'created_at', 'updated_at', 'related']))
            return '';
        if(in_array(self::getFieldType($field_name), ['check-box', 'check', 'switch', 'file-options', 'files-options', 'choose-parents']))
            return '';

        if(Str::startsWith($field_name, ['seo_', 'meta_']))
            return '';

        return 'required';
    }

    /**
     * return whether the item should have a link on it's index page
     */
    public function hasPublicURL()
    {
        return true;
    }

    /**
     * return whether the model should have a text search on it's index page
     */
    public static function showAdminSearch()
    {
        return true;
    }

    /**
     * return a list of fields that should be added to the top of the admin index pages
     * @return Array | null           list of query fieldnames as key, with friendly string names
     *                                [$field_name]="friendly string"
     *                                that will be passd as query parameters or null or empty array
     *                                the fieldnames do not have to reflect the fieldnames in the table, they can be shortened versions that will be passed as request parameters
     *                                as you will also be overriding the scopeIndexSearch function below using the Request() object to get the extra parameters
     */
    public static function getAdminSearchDropDowns() : Array | null
    {
        return null;
    }

    /**
     * return the dropdown list for the above search dropdown key
     * @param  String $field_name        the query fieldname posted when a query is submitted
     * @return Array | null              the list of valid options [value]=String where value is passed in the query, the first option should have a key of the empty string and will be selected by default
     */
    public static function getAdminSearchDropDownList($field_name) : Array | null
    {
        return null;

    }


    /**
     * return the dropdown list for the above search dropdown key
     * @param  String $field_name        the query fieldname posted when a query is submitted
     * @return String a type for the filter, empty string or unknown default is a drop down, 'date' should be implemented as a input/filter type too
     */
    public static function getAdminSearchDropDownType($field_name) : string
    {
        switch ($field_name) {
            case 'from':
            case 'to':
                return 'date';
                break;
            
            default:
                return '';
                break;
        }

        return '';
    }

    /**
     * Return a string for the Session variable that remembers something for the fieldname
     * @param  String $field_name        the query fieldname for the filter
     * @freturn String | null            a String for the session variable, default is field.$field_name
     */
    public static function getFieldSessionKey($field_name) : String | null
    {
        return "field_".$field_name;
    }

    /**
     * the search scope function for use in submodules that will
     * be called stically or via ajax
     * not required unless using tabbed submodules
     * @param         $query
     * @param  array  $params
     * @return 
     */
    public function scopeIndexSearchWith($query, $params)
    {   
        if(isset($params['q']) && !empty($params['q']))
        {
            $q = '%'.$params['q'].'%';
            if(Schema::hasColumn($this->getTable(),'name'))
                $query->orWhere('name', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'title'))
                $query->orWhere('title', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'desc'))
                $query->orWhere('desc', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'content'))
                $query->orWhere('content', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'sku'))
                $query->orWhere('sku', 'like', $q);
        }

        return $query;
    }

    /**
     * the standard search scope function which can be overriden to add extra parameters
     * @param         $query
     * @param  String $q
     * @return 
     */
    public function scopeIndexSearch($query, $q)
    {   
        if(! empty($q))
        {
            $q = "%{$q}%";
            if(Schema::hasColumn($this->getTable(),'name'))
                $query->orWhere('name', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'title'))
                $query->orWhere('title', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'desc'))
                $query->orWhere('desc', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'content'))
                $query->orWhere('content', 'like', $q);
            if(Schema::hasColumn($this->getTable(),'sku'))
                $query->orWhere('sku', 'like', $q);
        }

        return $query;
    }

    public function scopeAdminOrder($query)
    {
        $sort = Request()->bs4sort;
        if(!empty($sort) && in_array(strtolower($sort), ['asc','desc']) )
        {
            $fieldname = Request()->bs4field;

            if($fieldname=="name-title") //special case 
            {   if(Schema::hasColumn($this->getTable(),'name'))
                    return $query->orderBy('name', $sort);
                elseif(Schema::hasColumn($this->getTable(),'title'))
                    return $query->orderBy('title', $sort);
                elseif(Schema::hasColumn($this->getTable(),'userName'))
                    return $query->orderBy('userName', $sort);
                elseif(Schema::hasColumn($this->getTable(),'slug'))
                    return $query->orderBy('slug', $sort);
            } else
            {   
                return $query->orderBy($fieldname, $sort);
            }

        }

        if($this->orderable()) {
            return $query->orderBy($this->orderable());
        }

        return $query->orderBy('updated_at', 'desc');
    }

    public function scopeOrder($query)
    {
        if($this->orderable()) {
            return $query->orderBy($this->orderable());
        }

        return $query->orderBy('created_at', 'desc');
    }

    /**
     * return an array of the columns that will have the sortable buttons
     */
    public static function sortableColumns() : array
    {
        return ['name-title', 'author', 'created_at', 'updated_at', 'status'];
    }

    /**
     * return an array of the default standard columns to be hidden
     */
    public static function hideStandardColumns() : array
    {
        return [];
        // return ['name-title', 'author', 'created_at', 'updated_at', 'status', 'edit']
    }

    /**
     * extra columns names to be automagiucally added to the listings page, inserted before the dates column
     * @return array       empty array adds no extra columns, 
     *                      array['field_name'] = "label column heading text"
     */
    public static function getExtraListingColumnsA(): array
    {
        return [];
    }
    /**
     * extra columns names to be automagiucally added to the listings page, inserted after the status column beforte the actions column
     * @return array       empty array adds no extra columns, 
     *                      array['field_name'] = "label column heading text"
     */
    public static function getExtraListingColumnsB(): array
    {
        return [];
    }

    /**
     * count the media item fields in this model
     */
    public function hasMediaCount()
    {
        $hasMedia = 0;
        foreach($this->getFillableForEditTabs() as $field_name)
        if(($field_name=='media') || Str::startswith($field_name, 'media_') || ($this->getFieldType($field_name)=='media'))
            $hasMedia++;
        
        return $hasMedia;
    }

    /**
     * count the file item fields in this model
     */
    public function hasFileOptionsCount()
    {
        $hasFileOptions = 0;
        foreach($this->getFillableForEditTabs() as $field_name)
        if(in_array($this->getFieldType($field_name), ['file-options', 'files-options']))
            $hasFileOptions++;
        
        return $hasFileOptions;
    }

    /**
     * get list of tabs that exists for all fields of this record (excluding sidebar items)
     * @return Array  string index list of strings
     */
    public function getEditTabs()
    {
        $tabs = [];
        $tabs['content'] = 'Content'; //force Main tab regardless
        $hasMediaTab = false;
        
        foreach($this->getFillableForEditTabs() as $value)
        {
            $tabName = $this->getFieldTab($value);
            if(($tabName!='sidebar') && ($tabName!='media') && !isset($tabs[$tabName]))
                $tabs[Str::snake($tabName)] = Str::headline($tabName);
            
            if($tabName=='media')
                $hasMediaTab=true;
        }

        //always add media tab at the end
        if($hasMediaTab)
            $tabs['media'] = 'media';

        if (isset($tabs['seo'])) {
            unset($tabs['seo']);
            $tabs['seo'] = 'SEO';
        }

        return $tabs;
    }

    /** Code to be inserted at the start of a section */
    public function getSectionHtml($tab, $section) : ?string
    {
        return null;
    }
    /** Code to be inserted at the start of a section */
    public function getSectionViewName($tab, $section) : ?string
    {
        return null;
    }
    /** Code to be inserted at the start of a section */
    public function getSectionComponentName($tab, $section) : ?string
    {
        return null;
    }

    /**
     * Determines which CMS editor tab a given field belongs to.
     *
     * This is used to group fields into logical sections within the admin interface,
     * such as sidebar, media, SEO, or main content.
     *
     * Tab assignment rules:
     * - "sidebar" for core metadata and status-related fields
     * - "media" for media-related fields
     * - "seo" for SEO/meta-related fields
     * - "content" as the default fallback tab
     *
     * @param string $field_name The field name being evaluated.
     * 
     * @return string The name of the tab the field belongs to.
     */
    public function getFieldTab($field_name)
    {
        if(in_array($field_name, [ 'parent', 'active', 'status', 'featured', 'created_at', 'updated_at']))
            return 'sidebar';
        if( Str::startswith($field_name, 'media_') ||  Str::startswith($field_name, 'media-')|| ($field_name=='media') )
            return 'media';

        if( Str::startswith($field_name, 'seo_') || Str::startswith($field_name, 'seo-')  || Str::startswith($field_name, 'meta_')  || Str::startswith($field_name, 'meta-') )
            return 'seo';

        return 'content';
    }

     /**
     * return an array of test requirements, and filetypes/extensions
     * @param String   $field_name
     * @return Array   two elements, text requirements, and filetypes/extensions for the filetpe  input
     */
    public function getFieldUploadTypes($field_name)
    {
        if(in_array($field_name, ['downloads', 'download']))
        {
            $default =  [ 'PDF, DOC, DOCX, XLS, XLSX, ODT, ODS', 
                          '.pdf, .doc, .docx, .xls, .xlsx, .odt, .ods'];
            return backslash4::getDashboardConfigSetting('MediaManager.download_types', $default);
        }
        
        if( Str::startswith($field_name, 'media_') ||  Str::startswith($field_name, 'media-')|| ($field_name=='media') 
            || Str::startsWith($this->getFieldType($field_name), 'media') )
        {
            if(MediaManager::isVideosEnabled())
            {
                $image_types = MediaManager::getImageTypes();
                $video_types = MediaManager::getVideoTypes();
                return [  $image_types[0].', '. $video_types[0], 
                          $image_types[0].', '. $video_types[1] ];
            }

            //else
            return MediaManager::getImageTypes();
        } 

        return ['Any file', ''];
    }

    /**
     * Returns the field type used by the generic admin editor for a given field name.
     *
     * This method determines how a field should be rendered in the CMS based on
     * predefined conventions and naming rules.
     *
     * Models may override this method to provide custom field-type mappings
     * for specific use cases (see the Setting model for an example).
     *
     * Supported return values include UI editor types such as:
     * - id
     * - richtexteditor
     * - choose-options
     * - choose-parents
     * - money
     * - number
     * - files-options
     * - file-options
     * - switch
     * - date-time (and date-time:readonly)
     * - time
     * - media
     * - address
     * - tag-related
     * - not-on-form
     *
     * If no explicit rule matches, the method falls back to checking dynamic
     * model configuration (e.g. choose options or data lists), then applied
     * naming conventions (date_, media_, _address, etc.)
     *
     * @param string $field_name The database or model field name being evaluated.
     * 
     * @return string The resolved field type for the admin editor.
     */
    public function getFieldType($field_name)
    {
        switch ($field_name) {
            case 'id':
                return 'id';
                break;

            case 'content':
            case 'description':
            case 'summary':
            case 'question':
            case 'answer':
                return 'richtexteditor';
                break;

            case 'journey_tracker':
                return 'journey-tracker';
                break;

            case 'category':
            case 'type':        
                return 'choose-options';
                break;

            case 'parent':
                return 'choose-parents';
                break;

            case 'price': //prices stored in pennies
            case 'price2':
            case 'cost':
            case 'total_nett':
            case 'total_vat':
            case 'total_gross':
                return 'money';
                break;

            case 'qty':
            case 'quantity':
            case 'weight':
            case 'vat_rate':
                return 'number';
                break;

            case 'files':
            case 'downloads':
                return 'files-options';
                break;

            case 'file':
            case 'download':
                return 'file-options';
                break;

            case 'active':
            case 'status':
            case 'featured':
                return 'switch';
                break;

            case 'created_at':
            case 'updated_at':
                return 'date-time:readonly'; //meta tag, :readonly
                break;

            case 'related':
                return 'tag-related';
                break;

            case 'seo_keywords':
                return 'not-on-form'; // No longer a valid SEO related thing
                break;
                
        }
        //there is also 'readonly' which is never used for any defults
        //but you ay want to use it in your model

        if(!empty($this->getChooseOptions($field_name)))
            return 'choose-options';

        if(!empty($this->getDataList($field_name)))
            return 'data-list';

        if( Str::startswith($field_name, 'date_') || ($field_name=='date') )
            return 'date-time';

         if( Str::startswith($field_name, 'start_time') || ($field_name=='end_time') )
            return 'time';

        if( Str::startswith($field_name, 'media_') || ($field_name=='media') )
            return 'media';

        if(Str::endswith($field_name,'_address') || ($field_name=='address'))
            return "address";
        
        return $field_name;
    }

    /**
     * return an html string to be rendered without escaping before a CMS field in autogeneric
     * @return String    An html string to be rendered without escaping
     */
    public function renderBeforeField($field_name) : String
    {
        return '';
    }
    
    /**
     * return an html string to be rendered without escaping after a CMS field in autogeneric
     * @return String    An html string to be rendered without escaping
     */
    public function renderAfterField($field_name) : String
    {
        return '';
    }

    /**
     * return extra html (attributes) that will be rendered in the top level component in auto_generic comnponent for CMS fields
     * @param  String $field_name     a field name in the model
     * @return Array                  array of media sizes [ 'widthxheight'] ] - width is the only parameter used and may be x pixels or x% can be "width,height" "width;height"
     */
    public static function getFieldMediaSizes($field_name, $model=null) : array
    {
        $extrasizes=[];

        $cfsizes = config('backslash4.images.extrasizes',[]);
        if(!empty($cfsizes))
        {   foreach($cfsizes as $newsize)
                $extrasizes[]=$newsize;
        }
        
        if(!empty(config('backslash4.tenants-on',0)))
        {
            $tsizes=Backslash4::getTenantSetting('images.extrasizes',[]);
            if(!empty($tsizes))
            {   
                foreach($tsizes as $newsize)
                    $extrasizes[]=$newsize;
            }

            if(!empty($model)) {
                $tsizes=Backslash4::getTenantSetting(strtolower($model).'.extrasizes.'.$field_name,[]);
                foreach($tsizes as $newsize)
                    $extrasizes[]=$newsize;
            }
        }

        return $extrasizes;
    }

    /**
     * return extra html (attributes) that will be rendered in the top level component in auto_generic comnponent for CMS fields
     * @param  String $field_name     a field name in the model
     * @return String | Array         default empty string, if overridden extra html that will be rendered inside the descendant editor component
     */
    public function getFieldAttributes($field_name) : string | array
    {
        return '';
    }

    /**
     * whether this model needs the csrf tag at the top of the page
     */
    public function requiresCSRF()
    {
        return 0;
    }
    /**
     * Return extra info to put in the edit page sidebar
     * such as relevant non-editable info cross referenced from other tables
     * 
     * @return view   default null - test for empty
     */
    public function getExtraSidebarView()
    {
        return null;
    }

    /**
     * whether this item allows deletion on the admin index page
     */
    public function allowDelete() : bool
    {
        return true;
    }

    /**
     * Returns a view if there are any custom actions
     */
    public function customActions()
    {
        return '';

    }

    /**
     * whether this item allows deletion on the admin index page
     */
    public function allowEdit() : bool
    {
        return true;
    }
    
    /**
     * whether this item allows deletion on the admin index page
     */
    public function getEditName() : string
    {
        return 'Edit';
    }
    
    /**
     * Indicates whether this item type supports creation via the CMS admin index page.
     *
     * @return bool Whether the "Add New" button is visible in the CMS interface.
     */
    public static function showCMSAddNewButton() : bool
    {
        return true;
    }
    
    /**
     * return the URL for the add new button in the CMS
     * you may want to amend this ids your route has params such as a category in it
     */
    public static function cmsAddNewRouteUrl($routename) : string
    {
        return route($routename);
    }

    /**
     * Returns a human-readable status string for the model.
     *
     * If the model has an "active" column, it returns:
     * - "Active" when the model is in the active state
     * - "Inactive" otherwise
     *
     * @return string The status label, or an empty string if the "active" column does not exist.
     */
    public function getStatus() : String
    {
        if(Schema::hasColumn($this->getTable(),'active'))
            if($this->active == Model::STATE_ACTIVE)
                return "Active";
            else
                return "Inactive";
        
        return '';
    }

    /**
     * Return extra info to put in at the top of the list index page
     * such as relevant non-editable info cross referenced from other tables
     * 
     * @return view   default null - test for empty
     */
    public static function getExtraIndexHeaderView()
    {
        return null;
    }

    /**
     * Return extra info to put in at the top of the list index page
     * such as relevant non-editable info cross referenced from other tables
     * 
     * @return view   default null - test for empty
     */
    public static function getExtraIndexFooterView()
    {
        return null;
    }

    /**
     * Returns the bas class name of this object class model
     * @return String base class name
     */
    public function basename()
    {
        return (new \ReflectionClass($this))->getShortName();
    }

    public function getDashBoardSettings() : array
    {
        $menus = \Clevercherry\Backslash4\Backslash4::getDashboardConfig()['cmsMenu'];
        if(isset($menus[$this->getCMSModuleName()]))
            return $menus[$this->getCMSModuleName()];

        //else

        $menus = \Clevercherry\Backslash4\Backslash4::getDashboardFlatCMS();
        if(isset($menus[$this->getCMSModuleName()]))
            return $menus[$this->getCMSModuleName()];

        return [];
    }

    /**
    * return an array widget info
    * @return Array of widget info for display on the admin dashboard
    */
    public static function getDashboardWidgets()
    {
        return null;
    }

    /**
     * 
     */
    public function getEditTabInfoView($tabName = null)
     {
        return "
            <h2 class='font-medium mt-8 text-body text-lg'>".Str::singular(Str::headline($this->getTable())) ."</h2>
        ";
    }

    /**
     * @return string  the table name of the model
     */

    public static function getTableName() : string
    {
        return with(new static)->getTable();
    }
    
    
    /**
     * @return string  column width classes
     */

    public function getFieldWidth($fieldName)
    {
        return 'col-span-1';
    }
     

    /**
     * @param String $fieldName a field name from the model
     */

    public function isBreakAfterField($fieldName) {
       return false;
    }
     

    /**
     * @param String $fieldName a field name from the model
     */

    public function isBreakBeforeField($fieldName) {
       return false;
    }


    /**
     * Helper function to strip out html tags and return the string as a summary
     * @param String  $html
     * @param Integer $length   defaults to 255
     * @param String  $end      ending characters if string is truncated
     * @return String           Plain text string truncated to the gicven number of characters
     */
    public static function limitHtml($html, $length=255, $end='...')
    {
       return Str::limit(strip_tags($html), $length, $end);
    }

    /**
     * Does the listing page show an export button
     * @return String | null       null or empty string - do not show the button
     *                             String show an href button with the url
     *                             with the label of the string, ie. return "Export";
     *                             route('admin.MODEL.export') target="_blank"
     *                             or you cabn trap this button in some custom JavaScript
     */
    public static function exportUrl() : String | null
    {
       return null;
    }

    /**
     * @return Object | Array    list of downloads objects JSON decoced from download field
     */
    public function getDownloads() : Object | Array
    {
        if(empty(@$this->downloads))
            return [];

        return json_decode($this->downloads);        
    }

    /**
     * Generates a random string
     * @param  int length         default 64 length of the generated string
     * @param  string keyspace    characters allowed to be used in the string
     * @return string             a string of random characters of lenbvth size
     */
    public static function randomString(int $length = 64,
        string $keyspace = '0123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' ): string
     {
        if ($length < 1) {
            throw new \RangeException("Length must be a positive integer");
        }
        $pieces = [];
        $max = mb_strlen($keyspace, '8bit') - 1;
        for ($i = 0; $i < $length; ++$i) {
            $pieces []= $keyspace[random_int(0, $max)];
        }
        return implode('', $pieces);
    }

    /**
     * returns true if the module is not in the top level of the dashboard
     * this is slightly different from the Not Yet Implemented isTabbedModule
     * but close enough for use in presetting certain fields in the constructor of a model
     */
    public function isSubmodule() : bool
    {
        $menus = \Clevercherry\Backslash4\Backslash4::getDashboardConfig()['cmsMenu'];
        return !isset($menus[$this->getCMSModuleName()]);
    }

    /*
    public function isTabbedModule()
    {
        $menus = \Clevercherry\Backslash4\Backslash4::getDashboardConfig()['cmsMenu'];
        return !isset($menus[$this->getCMSModuleName()]);
    }
    */

    /**
     * return the $submodule['filters_hidden'] field from the config passed in
     * if the submodule is not passed gert it from thye config -- both usages within backslash already have the submodule
     * so why fetch it again
     * You may want to set it in the app (in a tenant situation where you'd rather do it in the model than in every tennat)
     * @retyurn Array      the filteer_deen array
     */
    public static function getHiddenFilters($submodule = null) : array
    {
        if($submodule==null)
        {   //both usages within backslash already have the submodule
            //so probably never called
            $submodule = Backslash4::getDashboardFlatCMS()[Str::Studly(self::getTableName())];
        }

        return isset($submodule['filters_hidden']) ? $submodule['filters_hidden'] : [] ;
    }

    /**
     * returns the default field value, this could be modified if it is a sub-module needing a setting from the parent
     * previous URL
     * This value is only queried if the provided value is null - you could also do some of this in the constructor
     * @return 
     */
    public function getDefaultFieldValue($field_name, $default=null)
    {
        return $default;
    }

    /**
     * convert a simple array of IDs (numbers) to an object containing that arrays as {'ids':array[]}
     * @param  array  $ids
     * @return Object           Object containing one item 'ids', which is the array
     */
    public static function arrayToIds(array $ids) : object
    {
        $object = new stdClass();
        $object->ids = $ids;
        return $object;
    }

    public function getFeaturedBadge($flag, $trueText = 'Featured', $falseText = 'Not featured') : string
    {
        switch ($flag) {
            case 0:
            case false:
                return "<div class='badge' data-theme='error'>{$falseText}</div>";
                break;
            case 2:
                return "<div class='badge' data-theme='gray'>{$trueText}</div>";
            case 1:
            case true:
                return "<div class='badge' data-theme='success'>{$trueText}</div>";
            default:
                return "<div class='badge' data-theme='success'>{$trueText}</div>";
        }
    }
}