// cat/src/compiler.js

const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');

/**
 * Parse a component file to extract the custom element name and its template.
 * @param {string} filePath - Path to the .comp.html file.
 * @returns {Promise<{ tagName: string, template: string }>} - Component details.
 */
async function parseComponent(filePath) {
  const content = await fs.readFile(filePath, 'utf-8');

  // Regex to match opening tags excluding <script> and <style>
  const tagRegex = /<(?!(script|style)\b)(\w[\w-]*)[^>]*>/i;
  const match = content.match(tagRegex);

  if (!match) {
    throw new Error(`No valid wrapper tag found in ${path.basename(filePath)}`);
  }

  let tagName = match[2];

  // If the tag name does not contain a hyphen, append '-cat'
  if (!tagName.includes('-')) {
    tagName = `${tagName}-cat`;
  }

  // Extract the inner HTML of the first valid wrapper tag
  const innerContentRegex = new RegExp(`<${match[2]}[^>]*>([\\s\\S]*?)<\\/${match[2]}>`, 'i');
  const innerMatch = content.match(innerContentRegex);

  const template = innerMatch ? innerMatch[1].trim() : '';

  return { tagName, template };
}

/**
 * Parse a page file to extract the route and its content.
 * @param {string} filePath - Path to the .page.html file.
 * @returns {Promise<{ route: string, content: string }>} - Page details.
 */
async function parsePage(filePath) {
  const content = await fs.readFile(filePath, 'utf-8');

  // Regex to match the <page> tag and extract the route attribute
  const pageTagRegex = /<page\s+[^>]*route=["']([^"']+)["'][^>]*>/i;
  const match = content.match(pageTagRegex);

  if (!match) {
    throw new Error(`No valid <page> tag with 'route' attribute found in ${path.basename(filePath)}`);
  }

  const route = match[1];

  // Extract the inner HTML of the <page> tag
  const innerContentRegex = new RegExp(`<page[^>]*>([\\s\\S]*?)<\\/page>`, 'i');
  const innerMatch = content.match(innerContentRegex);

  const pageContent = innerMatch ? innerMatch[1].trim() : '';

  return { route, content: pageContent };
}

/**
 * Detect used components in the provided HTML contents by parsing the HTML tags.
 * @param {Array<string>} htmlContents - Array of HTML file contents.
 * @param {Set<string>} availableComponents - Set of all available component tag names.
 * @returns {Set<string>} - Set of used component tag names.
 */
function detectUsedComponents(htmlContents, availableComponents) {
  const usedComponents = new Set();

  // Regex to match all tags in the HTML
  const tagRegex = /<\s*([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*>/g;

  htmlContents.forEach(content => {
    let match;
    while ((match = tagRegex.exec(content)) !== null) {
      let tag = match[1];
      // If tag doesn't contain '-', append '-cat'
      if (!tag.includes('-')) {
        tag = `${tag}-cat`;
      }
      if (availableComponents.has(tag)) {
        usedComponents.add(tag);
      }
    }
  });

  return usedComponents;
}

/**
 * Detect all <load> tags in the provided HTML contents and extract their routes.
 * @param {Array<string>} htmlContents - Array of HTML file contents.
 * @returns {Array<string>} - Array of routes found in <load> tags.
 */
function detectLoadRoutes(htmlContents) {
  const loadRoutes = [];

  // Regex to match <load route="..."> tags
  const loadTagRegex = /<load\s+[^>]*route=["']([^"']+)["'][^>]*>/gi;

  htmlContents.forEach(content => {
    let match;
    while ((match = loadTagRegex.exec(content)) !== null) {
      const route = match[1];
      loadRoutes.push(route);
    }
  });

  return loadRoutes;
}

/**
 * Generate the registration script for the used components.
 * @param {Map<string, string>} components - Map of tagName to template.
 * @returns {string} - JavaScript code for registering components.
 */
function generateRegistrationScript(components) {
  let script = '';

  components.forEach((template, tagName) => {
    const className = toPascalCase(tagName);
    script += `
class ${className} extends HTMLElement {
  constructor() {
    super();
  }
  connectedCallback() {
    this.innerHTML = \`${escapeBackticks(template)}\`;
  }
}
customElements.define('${tagName}', ${className});
`;
  });

  return script;
}

/**
 * Convert a kebab-case or other tag name to PascalCase for class naming.
 * @param {string} tagName - The custom element tag name.
 * @returns {string} - PascalCase string.
 */
function toPascalCase(tagName) {
  return tagName
    .split('-')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join('');
}

/**
 * Escape backticks in templates to avoid breaking template literals.
 * @param {string} str - The template string.
 * @returns {string} - Escaped string.
 */
function escapeBackticks(str) {
  return str.replace(/`/g, '\\`');
}

/**
 * Compiler Function
 * @param {string} indexPath - Path to the index.html file.
 * @returns {Promise<string>} - Compiled HTML content with component registrations and page injections.
 */
async function compile(indexPath) {
  // Read the index.html content
  let indexContent = await fs.readFile(indexPath, 'utf-8');

  // Define project root
  const projectRoot = path.dirname(indexPath);

  // -----------------------
  // Step 1: Process Components
  // -----------------------

  // Find all .comp.html files in the project directory
  const compFiles = glob.sync('**/*.comp.html', {
    cwd: projectRoot,
    absolute: true,
    ignore: ['node_modules/**', 'cat/**'] // Ignore node_modules and cat directory to prevent recursion
  });

  const componentMap = new Map(); // Map of tagName to template
  const parsedComponents = new Set(); // To track parsed components

  // Parse each component file
  for (const file of compFiles) {
    try {
      const { tagName, template } = await parseComponent(file);
      if (!parsedComponents.has(tagName)) {
        componentMap.set(tagName, template);
        parsedComponents.add(tagName);
        console.log(`🔍 Parsed component: <${tagName}> from ${path.relative(projectRoot, file)}`);
      } else {
        console.log(`ℹ️  Component <${tagName}> from ${path.relative(projectRoot, file)} has already been parsed.`);
      }
    } catch (error) {
      console.error(`❌ Failed to parse ${file}: ${error.message}`);
    }
  }

  if (componentMap.size === 0) {
    console.warn('⚠️  No components found to compile.');
  }

  // -----------------------
  // Step 2: Process Pages
  // -----------------------

  // Find all .page.html files in the project directory
  const pageFiles = glob.sync('**/*.page.html', {
    cwd: projectRoot,
    absolute: true,
    ignore: ['node_modules/**', 'cat/**']
  });

  const routeMap = new Map(); // Map of route to page content

  for (const file of pageFiles) {
    try {
      const { route, content } = await parsePage(file);
      if (routeMap.has(route)) {
        console.warn(`⚠️  Duplicate route "${route}" found in ${path.relative(projectRoot, file)}. Overwriting previous route.`);
      }
      routeMap.set(route, content);
      console.log(`📄 Mapped route "${route}" to ${path.relative(projectRoot, file)}`);
    } catch (error) {
      console.error(`❌ Failed to parse page ${file}: ${error.message}`);
    }
  }

  if (routeMap.size === 0) {
    console.warn('⚠️  No pages found to map routes.');
  }

  // -----------------------
  // Step 3: Detect Used Components
  // -----------------------

  // Find all .html files in the project directory for usage detection
  const htmlFiles = glob.sync('**/*.html', {
    cwd: projectRoot,
    absolute: true,
    ignore: ['node_modules/**', 'cat/**']
  });

  const htmlContents = [];
  for (const file of htmlFiles) {
    try {
      const content = await fs.readFile(file, 'utf-8');
      htmlContents.push(content);
    } catch (error) {
      console.error(`❌ Failed to read HTML file ${file}: ${error.message}`);
    }
  }

  // Detect which components are used across all HTML files
  const availableComponents = new Set(componentMap.keys());
  const usedComponents = detectUsedComponents(htmlContents, availableComponents);

  if (usedComponents.size === 0) {
    console.warn('⚠️  No used components found in HTML files.');
  } else {
    console.log(`📦 Used components: ${[...usedComponents].join(', ')}`);
  }

  // -----------------------
  // Step 4: Handle <load> Tags and Inject Page Content
  // -----------------------

  // Read all .html files and prepare to replace <load> tags
  // We will compile each .html file separately and map them to routes
  // For simplicity, let's assume that there's a main 'index.html' which is the entry point
  // and it contains <load> tags for different routes.

  // Alternatively, if multiple HTML files are to be compiled for different routes,
  // this can be adjusted accordingly.

  // For this implementation, we'll proceed with the main 'index.html' containing <load> tags.

  // Detect all <load> routes in all HTML files
  const loadRoutes = detectLoadRoutes(htmlContents);

  // Prepare a map of route to page content
  // Verify that each load route exists in the routeMap
  loadRoutes.forEach(route => {
    if (!routeMap.has(route)) {
      console.error(`❌ No page found for route "${route}". Please ensure a .page.html file exists with route="${route}".`);
    }
  });

  // Now, replace <load route="..."> tags in indexContent with the corresponding page content
  const loadTagRegex = /<load\s+[^>]*route=["']([^"']+)["'][^>]*>/gi;

  indexContent = indexContent.replace(loadTagRegex, (match, p1) => {
    const route = p1;
    if (routeMap.has(route)) {
      console.log(`🔄 Injecting content for route "${route}"`);
      return routeMap.get(route);
    } else {
      console.warn(`⚠️  No content found for route "${route}". Keeping <load> tag.`);
      return match; // Keep the <load> tag if no content is found
    }
  });

  // -----------------------
  // Step 5: Generate Registration Script
  // -----------------------

  const registrationScript = generateRegistrationScript(
    Array.from(usedComponents).reduce((map, tag) => map.set(tag, componentMap.get(tag)), new Map())
  );

  // -----------------------
  // Step 6: Inject Registration Script
  // -----------------------

  if (registrationScript.trim()) {
    // Inject the registration script before the closing </body> tag
    const scriptTag = `<script>
${registrationScript}
</script>
`;

    if (indexContent.includes('</body>')) {
      indexContent = indexContent.replace('</body>', `${scriptTag}</body>`);
    } else {
      // If no </body> tag, append the script at the end
      indexContent += `\n${scriptTag}`;
    }
    console.log('📝 Injected component registration scripts.');
  }

  // -----------------------
  // Step 7: Return Compiled Content
  // -----------------------

  return indexContent;
}

module.exports = { compile };

---

## Task

- **Parse Component Files**
    
    - Implement a parser to read `.comp.html` files and extract HTML, script, and style sections.
    - Identify expressions within attributes and text nodes using a pattern like `{expression}`.
    
- **Generate Class Structure**
    
    - Create a class for each component with a constructor that initializes default values.
    - Attach a shadow DOM to encapsulate styles and markup.
    
- **Build DOM Programmatically**
    
    - Convert the extracted HTML into JavaScript code that creates DOM elements using `document.createElement`.
    - Use `document.createDocumentFragment` for efficient initial rendering.
    - Cache node references for elements containing reactive expressions.
    
- **Create Getters and Setters**
    
    - For each reactive expression, generate a getter and setter in the class.
    - Ensure setters update the DOM directly using cached node references.
    - Use template literals for updating attributes and text content dynamically.
    
- **Implement Event Listeners**
    
    - Parse event listener definitions from the script section.
    - Attach event listeners to the appropriate DOM elements in the `connectedCallback`.


Compilation format sample
Component definition
```
<!-- greeting.comp.html -->
<greeting-card>
  <div class="card">
    <h1>Hello, {name}!</h1>
    <p>Today is {dayOfWeek}.</p>
    <button onclick="changeGreeting()">Change Greeting</button>
    <img src="{imageUrl}" alt="Greeting Image">
  </div>
</greeting-card>

<script>
let name = 'Alice';
let dayOfWeek = 'Monday';
let imageUrl = 'https://example.com/image.jpg';

function changeGreeting() {
  name = 'Bob';
}
</script>

<style>
.card {
  border: 1px solid #ccc;
  padding: 10px;
}
</style>
```

Compiled version
```
class GreetingCard extends HTMLElement {
  constructor() {
    super();
    this._name = 'Alice';
    this._dayOfWeek = 'Monday';
    this._imageUrl = 'https://example.com/image.jpg';

    this.attachShadow({ mode: 'open' });
    this._buildDOM();
  }

  _buildDOM() {
    const fragment = document.createDocumentFragment();
    const style = document.createElement('style');
    style.textContent = `
      .card {
        border: 1px solid #ccc;
        padding: 10px;
      }
    `;
    fragment.appendChild(style);

    const card = document.createElement('div');
    card.className = 'card';

    this._h1Name = document.createElement('h1');
    this._h1Name.textContent = `Hello, ${this._name}!`;
    card.appendChild(this._h1Name);

    this._pDayOfWeek = document.createElement('p');
    this._pDayOfWeek.textContent = `Today is ${this._dayOfWeek}.`;
    card.appendChild(this._pDayOfWeek);

    this._button = document.createElement('button');
    this._button.textContent = 'Change Greeting';
    card.appendChild(this._button);

    this._img = document.createElement('img');
    this._img.setAttribute('src', `${this._imageUrl}`);
    this._img.alt = 'Greeting Image';
    card.appendChild(this._img);

    fragment.appendChild(card);
    this.shadowRoot.appendChild(fragment);
  }

  connectedCallback() {
    this._button.addEventListener('click', this.changeGreeting.bind(this));
  }

  disconnectedCallback() {
    this._button.removeEventListener('click', this.changeGreeting.bind(this));
  }

  get name() { return this._name; }
  set name(value) {
    if (this._name !== value) {
      this._name = value;
      this._h1Name.textContent = `Hello, ${value}!`;
    }
  }

  get dayOfWeek() { return this._dayOfWeek; }
  set dayOfWeek(value) {
    if (this._dayOfWeek !== value) {
      this._dayOfWeek = value;
      this._pDayOfWeek.textContent = `Today is ${value}.`;
    }
  }

  get imageUrl() { return this._imageUrl; }
  set imageUrl(value) {
    if (this._imageUrl !== value) {
      this._imageUrl = value;
      this._img.setAttribute('src', `${value}`);
    }
  }

  changeGreeting() {
    this.name = 'Bob';
  }
}

customElements.define('greeting-card', GreetingCard);
```

Once you finish provide the compiler.js.