// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {TSESTree} from '@typescript-eslint/utils';
import path from 'path';

import {isLitHtmlTemplateCall} from './utils/lit.ts';
import {createRule} from './utils/ruleCreator.ts';

type AssignmentExpression = TSESTree.AssignmentExpression;
type TemplateElement = TSESTree.TemplateElement;

// Define MessageIds used in the rule
type MessageIds =|'missingCheckboxStylesImport'|'missingCheckboxStylesAdoption';

const FRONT_END_DIRECTORY = path.join(
    // @ts-expect-error
    import.meta.dirname,
    '..',
    '..',
    '..',
    'front_end',
);

// NOTE: the actual file is input.ts, but for the sake of importing we want
// input.js as that's what the import statement would reference.
const COMMON_INPUT_STYLES = path.join(FRONT_END_DIRECTORY, 'ui', 'components', 'input', 'input.js');

export default createRule<[], MessageIds>({
  name: 'inject-checkbox-styles',
  meta: {
    type: 'problem',

    docs: {
      description: 'Ensure common checkbox styles are imported in Lit components',
      category: 'Possible Errors',
    },
    messages: {
      // Define messages corresponding to MessageIds
      missingCheckboxStylesImport:
          'When rendering a checkbox, ensure the common checkbox styles are imported from components/input/input.ts.',
      missingCheckboxStylesAdoption:
          'When rendering a checkbox, ensure the common checkbox styles are adopted into the component shadow root.',
    },
    schema: []
  },
  defaultOptions: [],
  create: function(context) {
    const filename = context.filename;
    let foundInputStylesImport = false;
    let inputStylesImportedName: string|null = null;
    // Type the node explicitly
    let adoptedStyleSheetsCallNode: AssignmentExpression|null = null;
    // Use a more specific type for the set elements if possible, otherwise Node or TemplateElement
    const litCheckboxElements = new Set<TemplateElement>();
    return {
      TaggedTemplateExpression(node) {
        // Assuming isLitHtmlTemplateCall is typed appropriately in utils.ts
        if (!isLitHtmlTemplateCall(node)) {
          return;
        }

        const litNodesContainingCheckbox = node.quasi.quasis.filter(element => {
          // element is TemplateElement
          return element.value.raw.includes('type="checkbox"');
        });
        for (const quasiNode of litNodesContainingCheckbox) {
          // We store the node so we can use it as the basis for the ESLint error later.
          litCheckboxElements.add(quasiNode);
        }
      },

      ImportDeclaration(node) {
        if (foundInputStylesImport) {
          return;
        }

        // Ensure node.source.value is a string before resolving
        if (typeof node.source.value !== 'string') {
          return;
        }

        // Get the absolute path of the current file's directory, so we can
        // compare it to COMMON_INPUT_STYLES and see if the file does import the common styles.
        const absoluteDirectory = path.dirname(path.resolve(filename));
        // Use try-catch for path resolution as it might fail for invalid paths
        try {
          const fullImportPath = path.resolve(absoluteDirectory, node.source.value);
          foundInputStylesImport = fullImportPath === COMMON_INPUT_STYLES;
          if (foundInputStylesImport) {
            // Ensure specifiers exist and the first one has a local name
            if (node.specifiers && node.specifiers.length > 0 && node.specifiers[0].local) {
              inputStylesImportedName = node.specifiers[0].local.name;
            }
          }
        } catch (e) {
          // Ignore path resolution errors
          console.error(`Error resolving import path: ${node.source.value} in ${filename}`, e);
        }
      },

      AssignmentExpression(node: AssignmentExpression) {
        if (node.left.type === 'MemberExpression' && node.left.property.type === 'Identifier' &&
            node.left.property.name === 'adoptedStyleSheets') {
          adoptedStyleSheetsCallNode = node;
        }
      },

      'Program:exit'() {
        if (litCheckboxElements.size === 0) {
          // No checkboxes to check, so we are done.
          return;
        }
        if (!foundInputStylesImport) {
          for (const checkbox of litCheckboxElements) {
            context.report({
              node: checkbox,  // checkbox is TemplateElement
              messageId: 'missingCheckboxStylesImport',
            });
          }
          return;
        }

        if (!adoptedStyleSheetsCallNode) {
          for (const checkbox of litCheckboxElements) {
            context.report({
              node: checkbox,  // checkbox is TemplateElement
              messageId: 'missingCheckboxStylesAdoption',
            });
          }
          return;
        }

        // Ensure the right side is an ArrayExpression before accessing elements
        if (adoptedStyleSheetsCallNode.right.type !== 'ArrayExpression') {
          // Handle cases where adoptedStyleSheets is assigned something other than an array
          // This might indicate an error or an unexpected pattern.
          // Depending on requirements, you might report an error or just return.
          return;
        }

        const inputCheckboxStylesAdoptionReference = adoptedStyleSheetsCallNode.right.elements.find(elem => {
          // Ensure elem is not null and is a MemberExpression before accessing properties
          if (!elem || elem.type !== 'MemberExpression') {
            return false;
          }

          // Ensure object and property are Identifiers before accessing name
          if (elem.object.type !== 'Identifier' || elem.property.type !== 'Identifier') {
            return false;
          }

          // Check that if we imported the styles as `Input`, that the reference here matches.
          // Use non-null assertion for inputStylesImportedName as it's checked by foundInputStylesImport logic
          if (elem.object.name !== inputStylesImportedName) {
            return false;
          }

          if (elem.property.name !== 'checkboxStyles') {
            return false;
          }

          return true;
        });

        if (!inputCheckboxStylesAdoptionReference) {
          context.report({
            node: adoptedStyleSheetsCallNode,  // Report on the assignment expression node
            messageId: 'missingCheckboxStylesAdoption',
          });
        }
      }
    };
  }
});
