/*
 * Baxxon 
 * Copyright(c) 2000-2025 Lorne Taylor
 * MIT Licensed
 */

// Single Page Application Framework
// MVC pattern defines Controller, Viewer, and Model classes to handle data operations, UI rendering, filtering, sorting, and pagination for records via an API

import API from "./api.js";
import Service from "./services.js";
import op from "./spa-options.js";


// ==============================================
// CONTROLLER
// ==============================================

class Controller {

    defaultOp = {
        prefixId: "spa",
        targetId: "spa",
		api: {
			requestURI: "/spa-responder",
		},
        db: {},
		filters: {},         // { key: { type, default } }
        listFields: ["name"], // fields to show in list view
		metaDefaults: {},    // default fields for new records
		pagination: {
			maxRecords: 10,
		},
		backup: {
			path: "data-backups",
			auto: false,
			freq: 300, // 5 minutes
		},
		listTemplate: `
			{newBtn}
			{filters}
			{chips}
			{filterClearBtn}
			{sort}
			{count}
			{list}
			{pagination}
			{backup}
		`
    };

    constructor(op={}){

        this.op = { ...this.defaultOp, ...op };

		this.api = new API(this.op.api);
		this.userMode = 'NETWORK'; // user preference
		this.activeMode = 'NETWORK'; // actual operating state
		this.services = []; // Background service registry
		
        this.model = new Model(this.op,this.api);
        this.model.initState();
        this.data = {};

        this.uiElements();
        this.view = new Viewer({ ...this.op, ...this.elId });

        // 1. Map Events to Methods (The Registry)
        window.addEventListener('api-network-check', () => this.onNetworkPulse());
        window.addEventListener('spa-online',        () => this.onOnline());
        window.addEventListener('spa-offline',       () => this.onOffline());
		window.addEventListener('api-failure',       (e) => this.handleApiFailure(e.detail));
        window.addEventListener('spa-backup-auto',   () => this.onAutoBackup());
        window.addEventListener('DOMContentLoaded',  () => this.load());

        // 2. Register & Start Services
        this.registerService('Network Check', ()=>{
            window.dispatchEvent(new CustomEvent('api-network-check'));
        }, { freq: 60, persistent: true, allowManual: true }).start();

		this.registerService('Data Sync', async ()=>{
			if(this.model.dirty){
				await this.model.sync();
				this.data = this.model.data;
				await this.uiUpdateList();
			}
		}, { freq: 60, allowManual: true }).start(false);

		if(this.op.backup.auto){
			this.registerService('Backup Service', ()=>{
				window.dispatchEvent(new CustomEvent('spa-backup-auto'));
			}, { freq: this.op.backup.freq, allowManual: true }).start(false);
		}
    }

    // --------------------------------
    // EVENT HANDLERS 
    // --------------------------------

    async onOnline(){

        const syncService = this.services.find(s => s.key === 'Data Sync');
		if(syncService){
			syncService.run();
		}
        await this.uiUpdateList();
    }

    async onOffline(){

        console.warn("SPA: Gone offline, functioning in local mode.");
        await this.uiUpdateList();
    }

    async onNetworkPulse(){

        await this.checkConnectivity();
    }


    async onAutoBackup(){

        const res = await this.model.backup(this.op.backup.path);
        const service = this.services.find(s => s.key === 'Backup Service');
		if(service){
			if(res && res.success){
				service.setStatusMsg(`Last Success: ${new Date(res.date).toLocaleTimeString()}`);
			} else {
				service.setStatusMsg(`Failed: ${res?.err || 'Unknown'}`);
			}
		}
		this.uiUpdateList();
    }


	// --------------------------------
	// SERVICES
	// --------------------------------

	registerService(key, handler, options){

		const service = new Service(key, handler, options);
		this.services.push(service);
		return service;
	}

	startAutoServices(){

		console.log("SPA: Resuming all services");
		this.services.forEach(s => s.start());
	}

	stopAutoServices(){

		console.log("SPA: Pausing non-persistent services");
		// Stop non-persistent services (like backups) when offline
		this.services.filter(s => !s.op.persistent).forEach(s => s.stop());
	}

	async checkConnectivity(){

		const isConnected = await this.api.isReachable();
		if(!isConnected && this.activeMode === 'NETWORK'){
			await this.handleOffline();
		} 
		else if(isConnected && this.activeMode === 'LOCAL' && this.userMode === 'NETWORK'){
			await this.handleOnline();
		}
	}

	async handleOffline(){

		this.activeMode = 'LOCAL';
		this.api.online = false;
		console.warn("Application is operating in LOCAL mode");
		this.stopAutoServices(); 
		window.dispatchEvent(new CustomEvent('spa-offline'));
	}

	async handleOnline(){

		if (this.userMode === 'NETWORK') {
			this.activeMode = 'NETWORK';
			this.api.online = true;
			console.log("Application is back in NETWORK mode");
			
			// Refresh data from network upon reconnection
			this.data = await this.model.read(false);
			
			this.startAutoServices();
			window.dispatchEvent(new CustomEvent('spa-online'));
		}
	}

	handleApiFailure(error){
		
		console.error("API Failure detected:", error);
		if (this.activeMode === 'NETWORK') {
			this.handleOffline();
			this.uiUpdateList();
		}
	}


	// --------------------------------
	// UI
	// --------------------------------

    uiElements(){

        this.elId = {
            container: this.op.prefixId + "-container",
            list: this.op.prefixId + "-list",
            record: this.op.prefixId + "-record"
        };
    }
	
    async load(){

        await this.uiRender();

		const isLocal = this.userMode === 'LOCAL';
        this.data=await this.model.read(isLocal);
		await this.model.getBackupStatus(this.op.backup.path);
        await this.uiUpdateList();
    }

    async uiRender(action){

        const target = document.getElementById(this.op.targetId);
        const container = this.view.buildContainer(this.elId);

        target.replaceChildren();
        target.appendChild(container.el);

		switch(action){

			case "LIST":
				await this.uiUpdateList();
				break;

			case "RECORD":
				await this.uiUpdateRecord();
				break;
		}
    }

	// --------------------------------
	// LIST
	// --------------------------------

    async uiUpdateList(){

		// container
        const el = document.getElementById(this.elId.list);
        const components = {};

		// 0. Connectivity status & Mode Switcher
		components.status = this.view.buildStatus(this.activeMode);
		components.mode = this.view.buildModeSwitcher(this.userMode, this.activeMode);
		if(components.mode.el){
			components.mode.el.addEventListener("change", async (e) => {
				if(e.target.dataset.action !== "toggle-mode") return;
				
				const isNetwork = e.target.checked;
				const newMode = isNetwork ? 'NETWORK' : 'LOCAL';
				
				console.log(`SPA: User requested mode switch to ${newMode}`);
				this.userMode = newMode;
				
				if(newMode === 'LOCAL'){
					await this.handleOffline();
				} else {
					await this.checkConnectivity();
					if(this.activeMode !== 'NETWORK'){
						alert(`Switch to NETWORK failed. Last attempted: ${new Date().toLocaleTimeString()}`);
					}
				}
				await this.uiUpdateList();
			});
		}

		// 1. Create components and attach events
		
		// create new record 
		components.newBtn = this.view.buildCreateNewRecord();
		if(components.newBtn){
			components.newBtn.btn.addEventListener("click", () => {
				this.uiUpdateRecord();
				el.hidden = true;
			});
		}

		// filters
		const options = this.model.getFilterOptions(Object.values(this.data));
		components.filters = this.view.buildFilters(
			options,
			this.model.state.filters,
			this.model.op.filters
		);

		if(components.filters){
			components.filters.el.addEventListener("change", (e)=>{
				if (e.target.type !== "checkbox") return;

				const key = e.target.dataset.key;
				const val = e.target.value;

				if (!this.model.state.filters[key]){
					this.model.state.filters[key] = new Set();
				}

				if (e.target.checked){
					this.model.state.filters[key].add(val);
				} else {
					this.model.state.filters[key].delete(val);
					if (!this.model.state.filters[key].size){
						delete this.model.state.filters[key];
					}
				}
				this.model.saveToStorage();
				this.model.saveToURL();
				this.model.state.page = 1; // Reset to page 1
				this.uiUpdateList();
			});
		}

		// filter chips
		components.chips = this.view.buildFilterChips(this.model.state.filters);
		if(components.chips){
			components.chips.el.addEventListener("click", (e)=>{
				if (!e.target.dataset.key) return;

				const { key, val } = e.target.dataset;

				this.model.state.filters[key].delete(val);
				if (!this.model.state.filters[key].size){
					delete this.model.state.filters[key];
				}
				this.model.saveToStorage();
				this.model.saveToURL();
				this.model.state.page = 1; // Reset to page 1

				this.uiUpdateList();
			});
		}

		// clear filters
		components.filterClearBtn = this.view.buildFilterClear(this.model.state.filters);
		if(components.filterClearBtn){
			components.filterClearBtn.el.addEventListener("click", () => {
				this.model.state.filters = {};
				this.model.saveToStorage();
				this.model.saveToURL();
				this.model.state.page = 1; // Reset to page 1
				this.uiUpdateList();
			});
		}

		// sort
		components.sort = this.view.buildSort(this.op.sort, this.model.state.sort);
		if(components.sort){
			components.sort.el.addEventListener("change", (e) => {
				const [key, order] = e.target.value.split(":");
				if (key) {
					this.model.state.sort = [{ key, order }];
				} else {
					this.model.state.sort = [];
				}
				this.model.state.page = 1; // Reset to page 1
				this.uiUpdateList();
			});
		}

		// data 
        let dataArray=Object.values(this.data);
        dataArray=this.model.filter(dataArray);
        dataArray=this.model.sort(dataArray);

		// Pagination logic
		const totalRecords = dataArray.length;
		const maxRecords = this.op.pagination.maxRecords;
		const currentPage = this.model.state.page;

		// add record count under sort
		components.count = this.view.buildRecordCount(totalRecords, currentPage, maxRecords);
		
		dataArray = this.model.paginate(dataArray);

        components.list=this.view.buildList(dataArray);
        components.list.rowsById.forEach((row, id)=>{
            row.addEventListener("click", ()=>{
				this.uiUpdateRecord(id);
				el.hidden=true;
			});
        });

		// pagination UI
		components.pagination = this.view.buildListPagination(totalRecords, currentPage, maxRecords);
		if(components.pagination && components.pagination.el){
			components.pagination.el.addEventListener("click", (e)=>{
				if(e.target.tagName !== "BUTTON" || !e.target.dataset.page) return;
				this.model.state.page = parseInt(e.target.dataset.page);
				this.uiUpdateList();
			});
		}

		// services
		const syncService = this.services.find(s => s.key === 'Data Sync');
		if(syncService){
			if(this.model.dirty){
				syncService.setStatusMsg("Pending changes detected");
			} else {
				syncService.setStatusMsg("Synced");
			}
		}

		const serviceStatuses = this.services.map(s => s.getStatus());
		components.services = this.view.buildServices(serviceStatuses);
		if(components.services.el){
			components.services.el.addEventListener("click", (e) => {
				if(e.target.dataset.action !== "run-service") return;
				const key = e.target.dataset.key;
				const service = this.services.find(s => s.key === key);
				if(service){
					service.run();
					this.uiUpdateList();
				}
			});
		}

		// 2. Render layout
        el.replaceChildren();
		const layout = this.view.buildListLayout(components, this.op.listTemplate);
		el.appendChild(layout.el);

		// display list container
        el.hidden=false;
    }

	// --------------------------------
	// RECORD 
	// --------------------------------

    async uiUpdateRecord(recordId){

		// container
        const el=document.getElementById(this.elId.record);
        el.replaceChildren();

        const record = recordId ? this.data[recordId] : {};
        const recordForm = this.view.buildRecord(record, !recordId);

		// Record events
		
		// back/cancel button
        const backBtn=recordForm.el.querySelector("button[data-action='back'], button[data-action='cancel']");
        if(backBtn){

            backBtn.addEventListener("click", async ()=>{
				const elList=document.getElementById(this.elId.list);
				el.hidden=true;
				elList.hidden=false;
            });
        }

		// save button
        const saveBtn=recordForm.el.querySelector("button[data-action='save']");
        if (saveBtn){

            saveBtn.addEventListener("click", async ()=>{
                const updated = this.getFormData(recordForm.el);
				let newId = null;
				const isLocal = this.activeMode === 'LOCAL';

				if(recordId){
					newId = await this.model.update(recordId, updated, isLocal);
				} else {
					// for new records, we need a directory - default to 'test'
					newId = await this.model.create('test', updated, isLocal);
				}

                if(newId){
                    this.data = await this.model.read(isLocal);
                    await this.uiUpdateList();
					document.getElementById(this.elId.list).hidden = true;
					
                    this.uiUpdateRecord(newId);
                }
            });
        }

		// delete button
        const delBtn = recordForm.el.querySelector("button[data-action='delete']");
        if (delBtn){

            delBtn.addEventListener("click", async ()=>{
				
				// confirm delete
				if( window.confirm( "Are you sure you want to delete this record?" )){

					const isLocal = this.activeMode === 'LOCAL';
					const success = await this.model.delete(recordId, isLocal);
					if(success){
						this.data = await this.model.read(isLocal);
						await this.uiUpdateList();
						document.getElementById(this.elId.record).hidden = true;
						document.getElementById(this.elId.list).hidden = false;
					}
				}
            });
        }

        el.appendChild(recordForm.el);
        el.hidden=false;
    }

    getFormData(formEl){

        const inputs=formEl.querySelectorAll("input, textarea");
        const data={};
        inputs.forEach(input => { 
			if (input.name) data[input.name]=input.value; 
		});
        return data;
    }
}

// ==============================================
// VIEWER
// ==============================================

class Viewer {

    constructor(op = {}){ 

		this.op = op;
	}

    buildContainer(elId){

        const el=document.createElement("div");
        el.setAttribute("id", elId.container);

        const list=document.createElement("div");
        list.setAttribute("id", elId.list);
        el.appendChild(list);

        const record=document.createElement("div");
        record.setAttribute("id", elId.record);
        el.appendChild(record);

		const apiStat = document.createElement("div");
        apiStat.setAttribute("id", "api-stat");
        el.appendChild(apiStat);

        return { el };
    }


	// -----------------------------------
	// LIST 
	// -----------------------------------
	
	buildListLayout(components = {}, template = ""){

		const el = document.createElement("div");
		
		// 1. Prepare template by replacing {name} with temporary marker divs
		let html = template;
		Object.keys(components).forEach(name => {
			html = html.replace(`{${name}}`, `<div data-placeholder="${name}"></div>`);
		});
		
		el.innerHTML = html;

		// 2. Replace marker divs with actual component DOM elements
		const markers = el.querySelectorAll("[data-placeholder]");
		markers.forEach(marker => {
			const name = marker.dataset.placeholder;
			if (components[name] && components[name].el) {
				marker.replaceWith(components[name].el);
			} else {
				marker.remove();
			}
		});

		return { el };
	}

	buildFilters(options={}, active={}, config={}){

		const el = document.createElement("div");

		// Filter groups
		Object.entries(options).forEach(([key, map])=>{

			const group = document.createElement("div");

			const title = document.createElement("strong");
			title.textContent = config[key]?.label || key;
			group.appendChild(title);

			map.forEach((count, val)=>{

				const label = document.createElement("label");

				const input = document.createElement("input");
				input.type = "checkbox";
				input.dataset.key = key;
				input.value = val;

				if (active[key]?.has(val)){
					input.checked = true;
				}

				if (!count){
					input.disabled = true;
				}

				label.appendChild(input);
				label.append(` ${val} (${count})`);

				group.appendChild(label);
			});

			el.appendChild(group);
		});

		return { el };
	}

	buildFilterChips(filters={}){

		const hasActiveFilters = Object.values(filters).some(set => set && set.size);

		// hide entirely if no filters

		if (!hasActiveFilters){
			//return { el: document.createElement("div") };
			return null;
		}

		const el = document.createElement("div");

		// chips

		Object.entries(filters).forEach(([key, set])=>{

			set.forEach(val => {

				const chip = document.createElement("span");
				chip.className = "spa-filter-chip";
				chip.textContent = `${key}: ${val} ✕`;

				chip.dataset.key = key;
				chip.dataset.val = val;

				el.appendChild(chip);
			});
		});

		return { el };
	}

	buildFilterClear(active={}){

		// Clear button (only if filters active)
		const hasActiveFilters = Object.values(active).some(set => set && set.size);

		if(hasActiveFilters){
			const el = document.createElement("button");
			el.textContent = "Clear Filters";
			el.type = "button";
			el.dataset.action = "clear-filters";
			return { el };
		}
	}

	buildSort(config = {}, active = []){

		if (!Object.keys(config).length) return null;

        const el = document.createElement("div");
        const select = document.createElement("select");

        const defaultOpt = document.createElement("option");
        defaultOpt.value = "";
        defaultOpt.textContent = "Sort by...";
        select.appendChild(defaultOpt);

        Object.entries(config).forEach(([key, cfg])=>{

            if (key === "_body") return;

            // Ascending
            const optAsc = document.createElement("option");
            optAsc.value = `${key}:asc`;
            optAsc.textContent = `${cfg.label} (A-Z)`;
            if (active[0]?.key === key && active[0]?.order === 'asc') optAsc.selected = true;
            select.appendChild(optAsc);

            // Descending
            const optDesc = document.createElement("option");
            optDesc.value = `${key}:desc`;
            optDesc.textContent = `${cfg.label} (Z-A)`;
            if (active[0]?.key === key && active[0]?.order === 'desc') optDesc.selected = true;
            select.appendChild(optDesc);
        });

        el.appendChild(select);
        return { el };
    }

	buildCreateNewRecord(){

		const el=document.createElement("div");

		const btn = document.createElement("button");
		btn.textContent = "Create New Record";
		btn.dataset.action = "new";
		el.appendChild(btn);

		return { el, btn };
	}

	buildRecordCount(total, current, max){
		const el = document.createElement("div");
		el.className = "record-count";

		if (total === 0) {
			el.textContent = "No records found";
		} else {
			const start = (current - 1) * max + 1;
			const end = Math.min(current * max, total);
			el.textContent = `${start}-${end} of ${total} records`;
		}

		return { el };
	}

    buildList(data){

		// list container
        const el=document.createElement("div");
       
		// list data
        const table=document.createElement("table");
        const tbody=document.createElement("tbody");

        const rowsById=new Map();
        const rowsByClass=new Map();

        const fields = this.op.listFields || ["name"];

        // optional: thead
        if (fields.length > 1) {
            const thead = document.createElement("thead");
            const trH = document.createElement("tr");
            fields.forEach(field => {
                const th = document.createElement("th");
                // try to find label in filters or sort config
                const label = this.op.filters?.[field]?.label || this.op.sort?.[field]?.label || field;
                th.textContent = label;
                trH.appendChild(th);
            });
            thead.appendChild(trH);
            table.appendChild(thead);
        }

	// -----------------------------------
	// LIST 
	// -----------------------------------
        data.forEach(record => {

            const tr=document.createElement("tr");
            tr.dataset.id=record.id;
            
            fields.forEach(field => {
                const td=document.createElement("td");
                td.textContent=record[field] ?? (field === "name" ? record.id : "");
                tr.appendChild(td);
            });

            tbody.appendChild(tr);
            rowsById.set(record.id, tr);
        });

        table.appendChild(tbody);
        el.appendChild(table);

        return { el, rowsById, rowsByClass };
    }

	buildListPagination(totalRecords, currentPage, maxRecords){

		const totalPages = Math.ceil(totalRecords / maxRecords);
		if (totalPages <= 1) return { el: document.createElement("div") };

		const el = document.createElement("div");
		el.className = "pagination";

		// Prev Button
		const prevBtn = document.createElement("button");
		prevBtn.textContent = "Prev";
		prevBtn.disabled = currentPage === 1;
		prevBtn.dataset.page = currentPage - 1;
		el.appendChild(prevBtn);

		// Page Buttons (Max 5, 2 on either side of current)
		let startPage = Math.max(1, currentPage - 2);
		let endPage = Math.min(totalPages, startPage + 4);
		
		// Adjust start if we're near the end
		if (endPage - startPage < 4) {
			startPage = Math.max(1, endPage - 4);
		}

		for (let i = startPage; i <= endPage; i++) {
			const btn = document.createElement("button");
			btn.textContent = i;
			btn.dataset.page = i;
			if (i === currentPage) {
				btn.disabled = true;
				btn.style.fontWeight = "bold";
			}
			el.appendChild(btn);
		}

		// Next Button
		const nextBtn = document.createElement("button");
		nextBtn.textContent = "Next";
		nextBtn.disabled = currentPage === totalPages;
		nextBtn.dataset.page = currentPage + 1;
		el.appendChild(nextBtn);

		// Total Pages
		if (totalPages > 5) {
			const span = document.createElement("span");
			span.className = "page-total";
			span.textContent = ` of ${totalPages}`;
			el.appendChild(span);
		}

		return { el };
	}

	// -----------------------------------
	// SERVICES
	// -----------------------------------
	
	buildModeSwitcher(userMode, activeMode){
		const el = document.createElement("div");
		el.className = "network-mode-switcher";

		const labelText = document.createElement("span");
		labelText.className = "network-mode-label";
		labelText.textContent = "Network Mode:";
		el.appendChild(labelText);

		const toggle = document.createElement("label");
		toggle.className = "network-toggle";
		
		const checkbox = document.createElement("input");
		checkbox.type = "checkbox";
		checkbox.dataset.action = "toggle-mode";
		if(userMode === 'NETWORK') checkbox.checked = true;
		
		const slider = document.createElement("span");
		slider.className = "network-toggle-slider";
		
		toggle.appendChild(checkbox);
		toggle.appendChild(slider);
		el.appendChild(toggle);

		const badge = document.createElement("span");
		badge.className = `network-mode-badge ${activeMode.toLowerCase()}`;
		badge.textContent = `${activeMode}`;
		el.appendChild(badge);

		return { el, checkbox };
	}

	buildStatus(activeMode){
		const el = document.createElement("div");
		
		if (activeMode === 'LOCAL') {
			el.className = "status-monitor";
			el.textContent = "Local Mode Active - Changes will be synced when switched to Network";
		}

		return { el };
	}

	buildServices(statuses = []){
		const el = document.createElement("div");
		el.className = "api-services-list";

		statuses.forEach(s => {
			const item = document.createElement("div");
			item.className = "api-service-item";

			const dot = document.createElement("span");
			dot.className = `api-service-dot ${s.isRunning ? 'running' : 'stopped'}`;
			dot.title = s.isRunning ? "Running" : "Stopped";
			item.appendChild(dot);

			const text = document.createElement("span");
			text.className = "api-service-text";
			const lastRun = s.lastRun ? new Date(s.lastRun).toLocaleTimeString() : "Never";
			const statusMsg = s.statusMsg ? ` - ${s.statusMsg}` : "";
			text.textContent = `${s.key} (Freq: ${s.freq}s) - Last: ${lastRun}${statusMsg}`;


			if(s.allowManual){
				const btn = document.createElement("button");
				btn.textContent = "Run Now";
				btn.className = "api-service-run-btn";
				btn.dataset.action = "run-service";
				btn.dataset.key = s.key;
				// Disable if stopped (offline) unless persistent
				// But Service.run() handles logic, so we just enable it if service is running
				btn.disabled = !s.isRunning;
				item.appendChild(btn);
			}
			
			item.appendChild(text);

			el.appendChild(item);
		});

		return { el };
	}
	
	buildRecord(record, isNew = false){

		// record container
		const el=document.createElement("div");

		// record header
		el.appendChild( this.buildRecordHeader(isNew).el );

		// record data
		const form=document.createElement("form");
		form.className = "spa-record-form";

		// groups
		const specialKeys = ["id", "dateCreated", "dateModified"];
		
		// merge meta defaults if new
		const data = isNew ? { ...this.op.metaDefaults, ...record } : record;
		const entries = Object.entries(data);
		
		const metaFields = entries.filter(([key]) => !specialKeys.includes(key) && key !== "_body");
		const bodyField = entries.find(([key]) => key === "_body");
		const specialFields = Object.fromEntries(entries.filter(([key]) => specialKeys.includes(key)));

		// 1. Render editable metadata fields first
		metaFields.forEach(([key, val])=>{
			const label=document.createElement("label");
			label.textContent=key;
			form.appendChild(label);

			const input=document.createElement("input");
			input.type="text";
			input.name=key;
			input.value=val ?? "";
			form.appendChild(input);
			form.appendChild(document.createElement("br"));
		});

		// 2. Render _body (textarea)
		const bodyVal = bodyField ? bodyField[1] : (data._body ?? "");
		const textarea=document.createElement("textarea");
		textarea.name="_body";
		textarea.value=bodyVal ?? "";
		textarea.placeholder="add content here...";
		textarea.rows=12;

		form.appendChild(textarea);
		form.appendChild(document.createElement("br"));

		// 3. Render Read-Only footer fields in specific order: dateModified, dateCreated, id
		if (!isNew) {
			const footerOrder = ["dateModified", "dateCreated", "id"];
			footerOrder.forEach(key => {
				const val = specialFields[key];
				const label=document.createElement("label");
				label.textContent=key;
				form.appendChild(label);

				const span = document.createElement("span");
				span.className = "spa-record-footer-field";
				span.textContent = val ?? "";
				form.appendChild(span);
				form.appendChild(document.createElement("br"));
			});
		}

		// record actions
		form.appendChild( this.buildRecordActions(isNew).el );
		
		el.appendChild(form);
		return { el };
	}

	buildRecordHeader(isNew = false){
	
		// header container
		const el=document.createElement("div");
		el.innerHTML=isNew ? "<h3>Create New Record</h3>" : "<h3>Edit Record</h3>";

		return { el };
	}

	buildRecordActions(isNew = false){
	
		// record actions container
		const el=document.createElement("div");

		// record buttons
		if (isNew) {
			const cancelBtn=document.createElement("button");
			cancelBtn.type="button";
			cancelBtn.textContent="Cancel";
			cancelBtn.dataset.action="cancel";
			el.appendChild(cancelBtn);
		} else {
			const backBtn=document.createElement("button");
			backBtn.type="button";
			backBtn.textContent="Back";
			backBtn.dataset.action="back";
			el.appendChild(backBtn);
		}

		const saveBtn=document.createElement("button");
		saveBtn.id="spa-record-save";
		saveBtn.type="button";
		saveBtn.textContent="Save";
		saveBtn.dataset.action="save";
		el.appendChild(saveBtn);

		if (!isNew) {
			const delBtn=document.createElement("button");
			delBtn.id="spa-record-delete";
			delBtn.type="button";
			delBtn.textContent="Delete";
			delBtn.dataset.action="delete";
			el.appendChild(delBtn);
		}
	
		return { el };
	}
}


// ==============================================
// MODEL
// ==============================================

class Model {

    constructor(op, api){

        this.op = op;
		this.api = api;
        this.data = {};         // internal copy of records
        this.state = { 
			filters: {}, 
			sort: [],
			page: 1,
			lastBackup: null
		};
        this.dirty = false;
		this.localKey = "spa-data-" + (this.op.targetId || "default");
    }


	// ---------------------------------
	// STATE
	// ---------------------------------

    initState(){

        const defaults = this.getDefaultFilters();
        const stored   = this.loadFromStorage();
        const url      = this.loadFromURL();

        // precedence: defaults < storage < URL
        const merged = { ...defaults, ...stored, ...url };

        this.state.filters = this.toSet(merged);
    }

    getDefaultFilters(){

        const out = {};

        Object.entries(this.op.filters).forEach(([key, cfg])=>{
            if (cfg.default){
                out[key] = cfg.default;
            }
        });

        return out;
    }

	// ---------------------------------
	// STORAGE (Settings/Filters)
	// ---------------------------------

	saveToStorage(){

        const data = this.toArray(this.state.filters);

        localStorage.setItem("spa-filters", JSON.stringify(data));
    }

    loadFromStorage(){

        try{
            return JSON.parse(localStorage.getItem("spa-filters")) || {};
        }catch{
            return {};
        }
    }

	// ---------------------------------
	// LOCAL DATABASE (Records)
	// ---------------------------------

	saveToLocalDB(){

		localStorage.setItem(this.localKey, JSON.stringify(this.data));
	}

	loadFromLocalDB(){

		try{
			const raw = localStorage.getItem(this.localKey);
			return raw ? JSON.parse(raw) : {};
		} catch(err){
			console.error("LocalDB load failed", err);
			return {};
		}
	}


	// ---------------------------------
	// URL PARAMS
	// ---------------------------------

	loadFromURL(){

        const params = new URLSearchParams(window.location.search);
        const out    = {};

        params.forEach((val, key)=>{
            out[key] = val.split(",");
        });

        return out;
    }

    saveToURL(){

        const params = new URLSearchParams();
        const data   = this.toArray(this.state.filters);

        Object.entries(data).forEach(([key, vals])=>{
            if (vals.length){
                params.set(key, vals.join(","));
            }
        });

        const qs = params.toString();
        const url = qs ? "?" + qs : window.location.pathname;
        history.replaceState(null, "", url);
    }


	// ---------------------------------
	// CONVERSION
	// ---------------------------------
	
    toSet(obj){

        const out = {};

        Object.entries(obj).forEach(([k,v])=>{
            out[k] = new Set(v);
        });

        return out;
    }

    toArray(obj){

        const out = {};

        Object.entries(obj).forEach(([k,v])=>{
            out[k] = [...v];
        });

        return out;
    }

	
	// ---------------------------------
	// DATA
	// ---------------------------------
    // Read all records from DB via API
	
    async read(isLocal = false){

        if (isLocal) {
			this.data = this.loadFromLocalDB();
			return this.data;
		}

        try{
            const res = await this.api.send( 'load', { db: this.op.db });

            if(res.success){
				//console.log( res,res.data,res.success );
                this.data = res.data || {};
				this.saveToLocalDB(); // sync local cache
            }
            return this.data;
        } catch(err){
            console.error("API read failed", err);
            return this.read(true); // fallback to local
        }
    }

    // Create new record
    async create(dir, record, isLocal = false){

        if (isLocal) {
            const now = new Date().toISOString();
            const meta = { ...record, dateCreated: now, dateModified: now };
            const newId = this.buildId(dir, meta);
            if (!newId) return false;
            if (this.data[newId]) return false;

            this.data[newId] = { ...meta, _body: record._body || "", id: newId };
            this.dirty = true;
			this.saveToLocalDB();
            return newId;
        }

        try{
            const res = await this.api.send( 'add', { db: this.op.db, dir, record });

            if(res.success){
                this.data = res.data || this.data;
				this.saveToLocalDB();
                return res.recordId;
            }
        } catch(err){
            console.error("API create failed", err);
			return this.create(dir, record, true); // fallback
        }

        return false;
    }

    // Update existing record
    async update(recordId, record, isLocal = false){

        if (isLocal) {
            if (!this.data[recordId]) return false;
            
            const dir = recordId.split("/")[0];
            const now = new Date().toISOString();
            const merged = { ...this.data[recordId], ...record, dateModified: now };
            const newId = this.buildId(dir, merged);

            if (!newId) return false;
            if (newId !== recordId && this.data[newId]) return false;

            const updated = { ...merged, id: newId };
            if (newId !== recordId) {
                delete this.data[recordId];
            }
            this.data[newId] = updated;
            this.dirty = true;
			this.saveToLocalDB();
            return newId;
        }

        try{
            const res = await this.api.send( 'update', { db: this.op.db, recordId, record });

            if(res.success){
                this.data = res.data || this.data;
				this.saveToLocalDB();
                return res.recordId;
            }
        } catch(err){
            console.error("API update failed", err);
			return this.update(recordId, record, true); // fallback
        }

        return false;
    }

    // Delete record
    async delete(recordId, isLocal = false){

        if (isLocal) {
            if (!this.data[recordId]) return false;
            delete this.data[recordId];
            this.dirty = true;
			this.saveToLocalDB();
            return true;
        }

        try{
            const res = await this.api.send( 'delete', { db: this.op.db, recordId });
			
            if(res.success){
                this.data = res.data || this.data;
				this.saveToLocalDB();
                return true;
            }
        } catch(err){
            console.error("API delete failed", err);
			return this.delete(recordId, true); // fallback
        }

        return false;
    }

    async sync(){
        if (!this.api.online || !this.dirty) return;

        try {
            const res = await this.api.send('sync', { db: this.op.db, data: this.data });
            if (res.success) {
                this.data = res.data || this.data;
                this.dirty = false;
                console.log("SPA: Sync successful");
            }
        } catch (err) {
            console.error("SPA: Sync failed", err);
        }
    }

    buildId(dir, record){
        const parts = [];
        const fields = this.op.db.fileFields || [];
        const join = this.op.db.fileFieldsJoin || "-";

        for(const k of fields){
            if(!(k in record) || record[k] === ""){
                return undefined;
            }
            try{
                parts.push(this.safePart(record[k]));
            } catch {
                return undefined;
            }
        }

        const fileName = parts.join(join);
        if(fileName === "") return undefined;

        return `${dir}/${fileName}`;
    }

    safePart(v){
        let raw = String(v).trim();
        const spaces = this.op.db.fileSpaces || "";
        if (raw === "" || /[^a-zA-Z0-9\-_ ]/.test(raw)){
            throw new Error("safePart FAIL");
        }
        return raw.replace(/\s+/g, spaces);
    }

    // Backup data
    async backup(backupPath){
        try{
            const res = await this.api.send( 'backup', { db: this.op.db, backupPath });
            if (res.success && res.date) {
				this.state.lastBackup = res.date;
			}
			return res;
        } catch(err){
            console.error("API backup failed", err);
            return { success: false, err: err.message };
        }
    }

	// Get backup status
	async getBackupStatus(backupPath){
		try{
			const res = await this.api.send( 'getBackupStatus', { db: this.op.db, backupPath });
			if (res.success) {
				this.state.lastBackup = res.date;
			}
			return res;
		} catch(err){
			console.error("API getBackupStatus failed", err);
			return { success: false, err: err.message };
		}
	}


	// ---------------------------------
	// FILTERING
	// ---------------------------------

    getFilterOptions(data){

        const options = {};
        const keys    = Object.keys(this.op.filters).filter(k => k !== "_body");

        keys.forEach(k => options[k] = new Map());
        data.forEach(record => {
            keys.forEach(key => {
                
				const val = record[key] ?? "";
                if (!options[key].has(val)){
                    options[key].set(val, 0);
                }

                options[key].set(val, options[key].get(val) + 1);
            });
        });

        return options;
    }

    filter(data){

        const {filters} = this.state;
        const keys        = Object.keys(this.op.filters);

        return data.filter(record => {
            for(const key of keys){

                const selected = filters[key];
				if (!selected || !selected.size) continue;

                const value = record[key] ?? "";
                if(!selected.has(value)){
                    return false;
                }
            }

            return true;
        });
    }


	// ---------------------------------
	// SORTING
	// ---------------------------------
	
    sort(data){

        const sortRules = this.state.sort || [];
        if (!sortRules.length) return data;

        return [...data].sort((a, b) => {
            for (const { key, order } of sortRules) {
                
                const type = this.op.sort?.[key]?.type || 'string';
                let valA = a[key];
                let valB = b[key];

                let cmp = 0;

                switch(type) {
                    case 'number':
                        cmp = (Number(valA) || 0) - (Number(valB) || 0);
                        break;
                    case 'date':
                        cmp = new Date(valA || 0) - new Date(valB || 0);
                        break;
                    default: // string / alphabetical
                        cmp = String(valA || "").localeCompare(String(valB || ""), undefined, { numeric: true, sensitivity: 'base' });
                }

                if (cmp !== 0) return cmp * (order === "asc" ? 1 : -1);
            }
            return 0;
        });
    }

	// ---------------------------------
	// PAGINATION
	// ---------------------------------

	paginate(data){
		const max = this.op.pagination.maxRecords;
		const start = (this.state.page - 1) * max;
		return data.slice(start, start + max);
	}
}

const spa = new Controller(op);
export default spa;



