# Architecture Rules - Module Structure Rules
# Rules for app/modules/*/ directories

module_rules:

  - id: "MOD-001"
    name: "Self-contained modules must have required directories"
    severity: "error"
    description: |
      When a module declares is_self_contained=True in its definition,
      it must have the following directory structure:

      Required directories:
      - services/     - Business logic services (actual code, not re-exports)
      - models/       - Database models (actual code, not re-exports)
      - schemas/      - Pydantic schemas (actual code, not re-exports)
      - routes/       - API and page routes
        - routes/api/   - API endpoints
        - routes/pages/ - Page endpoints (if module has UI)

      Optional directories (based on module needs):
      - templates/    - Jinja templates (if module has UI)
      - static/       - Static assets (JS/CSS)
      - locales/      - Translation files
      - tasks/        - Celery tasks
      - exceptions.py - Module-specific exceptions

      WHY THIS MATTERS:
      - Consistency: All modules follow the same structure
      - Discoverability: Developers know where to find code
      - Encapsulation: Module owns all its components
      - Testability: Clear boundaries for unit tests
    pattern:
      directory_pattern: "app/modules/*/"
      required_when: "is_self_contained=True"
      required_dirs:
        - "services/"
        - "models/"
        - "schemas/"
        - "routes/"
        - "routes/api/"

  - id: "MOD-002"
    name: "Module services must contain actual code, not re-exports"
    severity: "warning"
    description: |
      Self-contained module services should contain actual business logic,
      not just re-exports from legacy locations.

      If a module's services/ directory only contains re-exports (from ...),
      the module is not truly self-contained.

      WRONG (re-export only):
        # app/modules/analytics/services/stats_service.py
        from app.services.stats_service import stats_service, StatsService
        __all__ = ["stats_service", "StatsService"]

      RIGHT (actual code):
        # app/modules/analytics/services/stats_service.py
        class StatsService:
            def get_stats(self, db: Session, vendor_id: int):
                # Actual implementation here
                ...
    pattern:
      file_pattern: "app/modules/*/services/*.py"
      anti_patterns:
        - "^from app\\.services\\."
        - "^from app\\/services\\/"

  - id: "MOD-003"
    name: "Module schemas must contain actual code, not re-exports"
    severity: "warning"
    description: |
      Self-contained module schemas should contain actual Pydantic models,
      not just re-exports from legacy models/schema/ location.

      WRONG (re-export only):
        # app/modules/analytics/schemas/stats.py
        from models.schema.stats import StatsResponse, ...
        __all__ = ["StatsResponse", ...]

      RIGHT (actual code):
        # app/modules/analytics/schemas/stats.py
        class StatsResponse(BaseModel):
            total: int
            pending: int
            ...
    pattern:
      file_pattern: "app/modules/*/schemas/*.py"
      anti_patterns:
        - "^from models\\.schema\\."
        - "^from models\\/schema\\/"

  - id: "MOD-004"
    name: "Module routes must use module-internal implementations"
    severity: "warning"
    description: |
      Module routes should import services from within the module,
      not from legacy app/services/ locations.

      WRONG:
        from app.services.stats_service import stats_service

      RIGHT:
        from app.modules.analytics.services import stats_service
        # or
        from ..services import stats_service
    pattern:
      file_pattern: "app/modules/*/routes/**/*.py"
      anti_patterns:
        - "from app\\.services\\."

  - id: "MOD-005"
    name: "Modules with UI must have templates and static directories"
    severity: "warning"
    description: |
      Modules that define menu_items (have UI pages) should have:
      - templates/{module_code}/admin/ or templates/{module_code}/vendor/
      - static/admin/js/ or static/vendor/js/

      This ensures:
      - UI is self-contained within the module
      - Templates are namespaced to avoid conflicts
      - JavaScript can be module-specific
    pattern:
      directory_pattern: "app/modules/*/"
      required_when: "has_menu_items=True"
      required_dirs:
        - "templates/"
        - "static/"

  - id: "MOD-006"
    name: "Module locales should exist for internationalization"
    severity: "info"
    description: |
      Self-contained modules should have a locales/ directory with
      translation files for internationalization.

      Structure:
        app/modules/{module}/locales/
          en.json
          de.json
          fr.json
          lu.json

      Translation keys are namespaced as {module}.key_name
    pattern:
      directory_pattern: "app/modules/*/"
      suggested_dirs:
        - "locales/"

  - id: "MOD-008"
    name: "Self-contained modules must have exceptions.py"
    severity: "warning"
    description: |
      Self-contained modules should have an exceptions.py file defining
      module-specific exceptions that inherit from WizamartException.

      Structure:
        app/modules/{module}/exceptions.py

      Example:
        # app/modules/analytics/exceptions.py
        from app.exceptions import WizamartException

        class AnalyticsException(WizamartException):
            """Base exception for analytics module."""
            pass

        class ReportGenerationError(AnalyticsException):
            """Error generating analytics report."""
            pass

      WHY THIS MATTERS:
      - Encapsulation: Module owns its exception hierarchy
      - Clarity: Clear which exceptions belong to which module
      - Testability: Easy to mock module-specific exceptions
    pattern:
      directory_pattern: "app/modules/*/"
      required_when: "is_self_contained=True"
      required_files:
        - "exceptions.py"

  # =========================================================================
  # Auto-Discovery Rules
  # =========================================================================

  - id: "MOD-009"
    name: "Module must have definition.py for auto-discovery"
    severity: "error"
    description: |
      Every module directory must have a definition.py file containing
      a ModuleDefinition instance for auto-discovery.

      The framework auto-discovers modules from app/modules/*/definition.py.
      Without this file, the module won't be registered.

      Required:
        app/modules/<code>/definition.py

      The definition.py must export a ModuleDefinition instance:
        # app/modules/analytics/definition.py
        from app.modules.base import ModuleDefinition

        analytics_module = ModuleDefinition(
            code="analytics",
            name="Analytics & Reporting",
            ...
        )
    pattern:
      directory_pattern: "app/modules/*/"
      required_files:
        - "definition.py"
        - "__init__.py"

  - id: "MOD-010"
    name: "Module routes must export router variable for auto-discovery"
    severity: "warning"
    description: |
      Route files (admin.py, vendor.py, shop.py) in routes/api/ and routes/pages/
      must export a 'router' variable for auto-discovery.

      The route discovery system looks for:
        - routes/api/admin.py with 'router' variable
        - routes/api/vendor.py with 'router' variable
        - routes/pages/vendor.py with 'router' variable

      Example:
        # app/modules/analytics/routes/pages/vendor.py
        from fastapi import APIRouter

        router = APIRouter()  # Must be named 'router'

        @router.get("/analytics")
        def analytics_page():
            ...
    pattern:
      file_pattern: "app/modules/*/routes/*/*.py"
      required_exports:
        - "router"

  - id: "MOD-011"
    name: "Module tasks must have __init__.py for Celery discovery"
    severity: "warning"
    description: |
      If a module has a tasks/ directory, it must have __init__.py
      for Celery task auto-discovery to work.

      Celery uses autodiscover_tasks() which requires the tasks
      package to be importable.

      Required structure:
        app/modules/<code>/tasks/
        ├── __init__.py           <- Required for discovery
        └── some_task.py

      The __init__.py should import task functions:
        # app/modules/billing/tasks/__init__.py
        from app.modules.billing.tasks.subscription import reset_period_counters
    pattern:
      directory_pattern: "app/modules/*/tasks/"
      required_files:
        - "__init__.py"

  - id: "MOD-012"
    name: "Module locales should have all supported language files"
    severity: "info"
    description: |
      Module locales/ directory should have translation files for
      all supported languages to ensure consistent i18n.

      Supported languages: en, de, fr, lu

      Structure:
        app/modules/<code>/locales/
        ├── en.json
        ├── de.json
        ├── fr.json
        └── lu.json

      Missing translations will fall back to English, but it's
      better to have all languages covered.
    pattern:
      directory_pattern: "app/modules/*/locales/"
      suggested_files:
        - "en.json"
        - "de.json"
        - "fr.json"
        - "lu.json"

  - id: "MOD-007"
    name: "Module definition must match directory structure"
    severity: "error"
    description: |
      If a module definition specifies paths like services_path, models_path,
      etc., those directories must exist and contain __init__.py files.

      Example: If definition.py has:
        services_path="app.modules.analytics.services"

      Then these must exist:
        app/modules/analytics/services/
        app/modules/analytics/services/__init__.py
    pattern:
      file_pattern: "app/modules/*/definition.py"
      validates:
        - "services_path -> services/__init__.py"
        - "models_path -> models/__init__.py"
        - "schemas_path -> schemas/__init__.py"
        - "exceptions_path -> exceptions.py or exceptions/__init__.py"
        - "templates_path -> templates/"
        - "locales_path -> locales/"

  # =========================================================================
  # Self-Contained Config & Migrations Rules
  # =========================================================================

  - id: "MOD-013"
    name: "Module config.py for environment-based configuration"
    severity: "info"
    description: |
      Self-contained modules can have a config.py file for environment-based
      configuration using Pydantic Settings.

      Structure:
        app/modules/<code>/config.py

      Pattern:
        # app/modules/marketplace/config.py
        from pydantic import Field
        from pydantic_settings import BaseSettings

        class MarketplaceConfig(BaseSettings):
            '''Configuration for marketplace module.'''

            # Settings prefixed with MARKETPLACE_ in environment
            api_timeout: int = Field(default=30, description="API timeout")
            batch_size: int = Field(default=100, description="Import batch size")

            model_config = {"env_prefix": "MARKETPLACE_"}

        # Export for auto-discovery
        config_class = MarketplaceConfig
        config = MarketplaceConfig()

      The config is auto-discovered by app/modules/config.py and can be
      accessed via get_module_config("marketplace").

      WHY THIS MATTERS:
      - Encapsulation: Module owns its configuration
      - Environment: Settings loaded from environment variables
      - Validation: Pydantic validates configuration on startup
      - Defaults: Sensible defaults in code, overridable via env
    pattern:
      directory_pattern: "app/modules/*/"
      suggested_files:
        - "config.py"

  - id: "MOD-014"
    name: "Module migrations must follow naming convention"
    severity: "warning"
    description: |
      If a module has migrations, they must:
      1. Be located in migrations/versions/ directory
      2. Follow the naming convention: {module_code}_{sequence}_{description}.py

      Structure:
        app/modules/<code>/migrations/
        ├── __init__.py
        └── versions/
            ├── __init__.py
            ├── <code>_001_initial.py
            ├── <code>_002_add_feature.py
            └── ...

      Example for cms module:
        app/modules/cms/migrations/versions/
        ├── cms_001_create_content_pages.py
        ├── cms_002_add_sections.py
        └── cms_003_add_media_library.py

      Migration file must include:
        revision = "cms_001"
        down_revision = None  # or previous revision
        branch_labels = ("cms",)  # Module-specific branch

      WHY THIS MATTERS:
      - Isolation: Module migrations don't conflict with core
      - Ordering: Sequence numbers ensure correct order
      - Traceability: Clear which module owns each migration
      - Rollback: Can rollback module migrations independently
    pattern:
      directory_pattern: "app/modules/*/migrations/versions/"
      file_pattern: "{module_code}_*.py"

  - id: "MOD-015"
    name: "Module migrations directory must have __init__.py files"
    severity: "warning"
    description: |
      If a module has a migrations/ directory, both the migrations/
      and migrations/versions/ directories must have __init__.py files
      for proper Python package discovery.

      Required structure:
        app/modules/<code>/migrations/
        ├── __init__.py           <- Required
        └── versions/
            └── __init__.py       <- Required

      The __init__.py files can be empty or contain docstrings:
        # migrations/__init__.py
        '''Module migrations.'''

      WHY THIS MATTERS:
      - Alembic needs to import migration scripts as Python modules
      - Without __init__.py, the directories are not Python packages
    pattern:
      directory_pattern: "app/modules/*/migrations/"
      required_files:
        - "__init__.py"
        - "versions/__init__.py"

  # =========================================================================
  # Legacy Location Rules (Auto-Discovery Enforcement)
  # =========================================================================

  - id: "MOD-016"
    name: "Routes must be in modules, not app/api/v1/"
    severity: "error"
    description: |
      All API routes must be defined in module directories, not in legacy
      app/api/v1/vendor/ or app/api/v1/admin/ locations.

      WRONG (legacy location):
        app/api/v1/vendor/orders.py
        app/api/v1/admin/orders.py

      RIGHT (module location):
        app/modules/orders/routes/api/vendor.py
        app/modules/orders/routes/api/admin.py

      Routes in modules are auto-discovered and registered. Legacy routes
      require manual registration and don't follow module patterns.

      EXCEPTIONS (allowed in legacy):
        - __init__.py (router aggregation)
        - auth.py (core authentication - will move to tenancy)
        - Files with # noqa: mod-016 comment

      WHY THIS MATTERS:
      - Auto-discovery: Module routes are automatically registered
      - Encapsulation: Routes belong with their domain logic
      - Consistency: All modules follow the same pattern
      - Maintainability: Easier to understand module boundaries
    pattern:
      prohibited_locations:
        - "app/api/v1/vendor/*.py"
        - "app/api/v1/admin/*.py"
      exceptions:
        - "__init__.py"
        - "auth.py"

  - id: "MOD-017"
    name: "Services must be in modules, not app/services/"
    severity: "error"
    description: |
      All business logic services must be defined in module directories,
      not in the legacy app/services/ location.

      WRONG (legacy location):
        app/services/order_service.py

      RIGHT (module location):
        app/modules/orders/services/order_service.py

      EXCEPTIONS (allowed in legacy):
        - __init__.py (re-exports for backwards compatibility)
        - Files that are pure re-exports from modules
        - Files with # noqa: mod-017 comment

      WHY THIS MATTERS:
      - Encapsulation: Services belong with their domain
      - Clear boundaries: Know which module owns which service
      - Testability: Can test modules in isolation
      - Refactoring: Easier to move/rename modules
    pattern:
      prohibited_locations:
        - "app/services/*.py"
      exceptions:
        - "__init__.py"

  - id: "MOD-018"
    name: "Tasks must be in modules, not app/tasks/"
    severity: "error"
    description: |
      All Celery background tasks must be defined in module directories,
      not in the legacy app/tasks/ location.

      WRONG (legacy location):
        app/tasks/subscription_tasks.py

      RIGHT (module location):
        app/modules/billing/tasks/subscription.py

      The module tasks/ directory must have __init__.py for Celery
      autodiscovery to work.

      EXCEPTIONS (allowed in legacy):
        - __init__.py (Celery app configuration)
        - dispatcher.py (task routing infrastructure)
        - Files with # noqa: mod-018 comment

      WHY THIS MATTERS:
      - Auto-discovery: Celery finds tasks from module directories
      - Encapsulation: Tasks belong with their domain logic
      - Consistency: All async operations in one place per module
    pattern:
      prohibited_locations:
        - "app/tasks/*.py"
      exceptions:
        - "__init__.py"
        - "dispatcher.py"

  - id: "MOD-019"
    name: "Schemas must be in modules, not models/schema/"
    severity: "error"
    description: |
      All Pydantic schemas must be defined in module directories,
      not in the legacy models/schema/ location.

      WRONG (legacy location):
        models/schema/order.py

      RIGHT (module location):
        app/modules/orders/schemas/order.py

      EXCEPTIONS (allowed in legacy):
        - __init__.py (re-exports for backwards compatibility)
        - base.py (base schema classes - infrastructure)
        - auth.py (core authentication schemas - cross-cutting)
        - Files with # noqa: mod-019 comment

      WHY THIS MATTERS:
      - Encapsulation: Schemas belong with their domain
      - Co-location: Request/response schemas near route handlers
      - Clear ownership: Know which module owns which schema
    pattern:
      prohibited_locations:
        - "models/schema/*.py"
      exceptions:
        - "__init__.py"
        - "base.py"
        - "auth.py"

  # =========================================================================
  # Module Definition Completeness Rules
  # =========================================================================

  - id: "MOD-020"
    name: "Module definition must have required attributes"
    severity: "warning"
    description: |
      Module definitions should include at minimum:
      - code: Module identifier
      - name: Human-readable name
      - description: What the module does
      - version: Semantic version
      - features: List of features (unless infrastructure module)
      - permissions: Access control definitions (unless internal or storefront-only)

      EXAMPLES (incomplete):
        module = ModuleDefinition(
            code="cart",
            name="Shopping Cart",
            description="...",
            version="1.0.0",
            # Missing features and permissions
        )

      EXAMPLES (complete):
        module = ModuleDefinition(
            code="orders",
            name="Order Management",
            description="...",
            version="1.0.0",
            features=["order_management", "fulfillment_tracking"],
            permissions=[
                PermissionDefinition(id="orders.view", ...),
            ],
        )

      EXCEPTIONS:
        - is_internal=True modules may skip permissions
        - Infrastructure modules (is_core=True with no UI) may skip features
        - Storefront-only modules (session-based, no admin UI) may have minimal permissions

      WHY THIS MATTERS:
      - Consistency: All modules follow the same definition pattern
      - RBAC: Permissions enable proper role-based access control
      - Feature flags: Features enable selective module functionality
    pattern:
      file_pattern: "app/modules/*/definition.py"
      required_attributes:
        - "code"
        - "name"
        - "description"
        - "version"

  - id: "MOD-021"
    name: "Modules with menus should have features"
    severity: "warning"
    description: |
      If a module defines menu items or menu sections, it should also
      define features to describe what functionality it provides.

      Menus indicate the module has UI and user-facing functionality,
      which should be documented as features.

      WRONG:
        module = ModuleDefinition(
            code="billing",
            menus={
                FrontendType.ADMIN: [...],
            },
            # Missing features list
        )

      RIGHT:
        module = ModuleDefinition(
            code="billing",
            features=[
                "subscription_management",
                "billing_history",
                "invoice_generation",
            ],
            menus={
                FrontendType.ADMIN: [...],
            },
        )

      WHY THIS MATTERS:
      - Documentation: Features describe what the module does
      - Feature flags: Enables/disables specific functionality
      - Consistency: All UI modules describe their capabilities
    pattern:
      file_pattern: "app/modules/*/definition.py"
      validates:
        - "menus -> features"

  - id: "MOD-022"
    name: "Feature modules should have permissions"
    severity: "info"
    description: |
      Modules with features should define permissions unless:
      - is_internal=True (internal tools like dev_tools)
      - Storefront-only module (session-based, no authentication)

      Permissions enable role-based access control (RBAC) for module
      functionality.

      EXCEPTIONS:
        - is_internal=True modules (internal tooling)
        - Modules with only storefront features (cart, checkout without admin UI)
        - Infrastructure modules (contracts, core utilities)

      EXAMPLE:
        module = ModuleDefinition(
            code="billing",
            features=["subscription_management", ...],
            permissions=[
                PermissionDefinition(
                    id="billing.view_subscriptions",
                    label_key="billing.permissions.view_subscriptions",
                    description_key="billing.permissions.view_subscriptions_desc",
                    category="billing",
                ),
            ],
        )

      WHY THIS MATTERS:
      - RBAC: Permissions enable proper access control
      - Security: Restrict who can access module features
      - Consistency: All feature modules define their access rules
    pattern:
      file_pattern: "app/modules/*/definition.py"
      validates:
        - "features -> permissions"
      exceptions:
        - "is_internal=True"

  - id: "MOD-023"
    name: "Modules with routers should use get_*_with_routers pattern"
    severity: "info"
    description: |
      Modules that define routers (admin_router, vendor_router, etc.)
      should follow the lazy import pattern with a dedicated function:

        def get_{module}_module_with_routers() -> ModuleDefinition:

      This pattern:
      1. Avoids circular imports during module initialization
      2. Ensures routers are attached at the right time
      3. Provides a consistent API for router registration

      WRONG:
        # Direct router assignment at module level
        module.admin_router = admin_router

      RIGHT:
        def _get_admin_router():
            from app.modules.orders.routes.admin import admin_router
            return admin_router

        def get_orders_module_with_routers() -> ModuleDefinition:
            orders_module.admin_router = _get_admin_router()
            return orders_module

      WHY THIS MATTERS:
      - Prevents circular imports
      - Consistent pattern across all modules
      - Clear API for module registration
    pattern:
      file_pattern: "app/modules/*/definition.py"
      validates:
        - "router imports -> get_*_with_routers function"

  # =========================================================================
  # Static File Mounting Rules
  # =========================================================================

  - id: "MOD-024"
    name: "Module static file mount order - specific paths first"
    severity: "error"
    description: |
      When mounting module static files in main.py, more specific paths must
      be mounted BEFORE less specific paths. FastAPI processes mounts in
      registration order.

      WRONG ORDER (locales 404):
        # Less specific first - intercepts /static/modules/X/locales/*
        app.mount("/static/modules/X", StaticFiles(...))
        # More specific second - never reached!
        app.mount("/static/modules/X/locales", StaticFiles(...))

      RIGHT ORDER (locales work):
        # More specific first
        app.mount("/static/modules/X/locales", StaticFiles(...))
        # Less specific second - catches everything else
        app.mount("/static/modules/X", StaticFiles(...))

      This applies to all nested static file mounts:
        - locales/ must be mounted before static/
        - img/ or css/ subdirectories must be mounted before parent

      SYMPTOMS OF WRONG ORDER:
        - 404 errors for nested paths like /static/modules/tenancy/locales/en.json
        - Requests to subdirectories served as 404 instead of finding files

      See: docs/architecture/user-context-pattern.md (Static File Mount Order section)
    pattern:
      file_pattern: "main.py"
      validates:
        - "module_locales mount BEFORE module_static mount"
