const fs = require('fs')
const path = require('path')
const { spawn } = require('child_process')

const rootDir = path.resolve(__dirname, '..')
const ignoredDirectories = new Set([
    '.git',
    '.cursor',
    '.vscode',
    '.docker',
    'docs',
    'local-ghost',
    'node_modules',
    'seed',
    'theme-export'
])
const ignoredFiles = new Set([
    '.DS_Store'
])
const syncExtensions = new Set([
    '.css',
    '.hbs',
    '.js',
    '.json',
    '.svg',
    '.yaml',
    '.yml'
])
const watchedRoots = [rootDir]

let syncTimer = null
let syncProcess = null
let scanTimer = null
let previousSnapshot = new Map()

/**
 * @param {string} relativePath
 */
function shouldIgnorePath(relativePath) {
    if (!relativePath || relativePath === '.') {
        return false
    }

    const segments = relativePath.split(path.sep)
    return segments.some((segment) => ignoredDirectories.has(segment) || ignoredFiles.has(segment))
}

/**
 * @param {string} relativePath
 */
function shouldSyncFile(relativePath) {
    if (shouldIgnorePath(relativePath)) {
        return false
    }

    if (relativePath === path.join('assets', 'styles', 'tailwind.css')) {
        return false
    }

    const extension = path.extname(relativePath)
    return syncExtensions.has(extension)
}

/**
 * @param {string} command
 * @param {string[]} args
 * @param {(code: number | null) => void} [onExit]
 */
function runCommand(command, args, onExit) {
    const child = spawn(command, args, {
        cwd: rootDir,
        stdio: 'inherit',
        shell: false
    })

    child.on('exit', (code) => {
        if (onExit) {
            onExit(code)
        }
    })

    return child
}

function queueSync() {
    if (syncTimer) {
        clearTimeout(syncTimer)
    }

    syncTimer = setTimeout(() => {
        if (syncProcess) {
            queueSync()
            return
        }

        console.log('[dev:watch] theme sync')
        syncProcess = runCommand('npm', ['run', 'dev:sync'], (code) => {
            syncProcess = null

            if (code !== 0) {
                console.error(`[dev:watch] sync failed with code ${code}`)
            }
        })
    }, 180)
}

/**
 * @param {string} directoryPath
 * @param {Map<string, number>} snapshot
 */
function collectSnapshot(directoryPath, snapshot) {
    if (!fs.existsSync(directoryPath)) {
        return
    }

    for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
        const entryPath = path.join(directoryPath, entry.name)
        const relativePath = path.relative(rootDir, entryPath)

        if (shouldIgnorePath(relativePath)) {
            continue
        }

        if (entry.isDirectory()) {
            collectSnapshot(entryPath, snapshot)
            continue
        }

        if (shouldSyncFile(relativePath)) {
            snapshot.set(relativePath, fs.statSync(entryPath).mtimeMs)
        }
    }
}

function buildSnapshot() {
    const snapshot = new Map()

    for (const watchedRoot of watchedRoots) {
        collectSnapshot(watchedRoot, snapshot)
    }

    return snapshot
}

function scanForChanges() {
    const nextSnapshot = buildSnapshot()

    for (const [relativePath, modifiedTime] of nextSnapshot.entries()) {
        if (previousSnapshot.get(relativePath) !== modifiedTime) {
            console.log(`[dev:watch] change detected: ${relativePath}`)
            queueSync()
            previousSnapshot = nextSnapshot
            return
        }
    }

    for (const relativePath of previousSnapshot.keys()) {
        if (!nextSnapshot.has(relativePath)) {
            console.log(`[dev:watch] change detected: ${relativePath}`)
            queueSync()
            previousSnapshot = nextSnapshot
            return
        }
    }

    previousSnapshot = nextSnapshot
}

function startPolling() {
    previousSnapshot = buildSnapshot()
    scanTimer = setInterval(scanForChanges, 800)
}

function shutdown() {
    if (syncTimer) {
        clearTimeout(syncTimer)
    }

    if (tailwindProcess && !tailwindProcess.killed) {
        tailwindProcess.kill('SIGINT')
    }

    if (syncProcess && !syncProcess.killed) {
        syncProcess.kill('SIGINT')
    }

    if (scanTimer) {
        clearInterval(scanTimer)
    }

    process.exit(0)
}

console.log('[dev:watch] initial prepare')

runCommand('npm', ['run', 'dev:prepare'], (prepareCode) => {
    if (prepareCode !== 0) {
        process.exit(prepareCode || 1)
    }

    console.log('[dev:watch] tailwind watch start')
    tailwindProcess = runCommand('npm', ['run', 'build:tailwind', '--', '--watch'])

    console.log('[dev:watch] filesystem watch start')
    startPolling()
})

let tailwindProcess = null

process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
