# Admin Feature Integration Guide

## Overview

This guide provides a step-by-step process for adding new admin features to the multi-tenant e-commerce platform. It ensures consistency with the established architecture patterns, naming conventions, and best practices.

## Table of Contents

1. [Planning Phase](#planning-phase)
2. [Database Layer](#database-layer)
3. [Schema Layer (Pydantic)](#schema-layer-pydantic)
4. [Exception Layer](#exception-layer)
5. [Service Layer](#service-layer)
6. [API Layer (Endpoints)](#api-layer-endpoints)
7. [Frontend Layer](#frontend-layer)
8. [Testing](#testing)
9. [Documentation](#documentation)
10. [Deployment Checklist](#deployment-checklist)

---

## Planning Phase

### 1. Define the Feature

**Questions to Answer:**
- What is the feature name? (Use singular for entity, plural for collections)
- What business problem does it solve?
- What are the CRUD operations needed?
- What relationships exist with other entities?
- What validation rules apply?
- What permissions are required?

**Example: Adding "Vendor Domains" Feature**
```
Feature Name: Vendor Domains
Purpose: Allow vendors to use custom domains
Operations: Create, Read, Update, Delete, Verify
Relationships: Belongs to Vendor
Validation: Domain format, uniqueness, DNS verification
Permissions: Admin only
```

### 2. Review Naming Conventions

**Consult:** `6_complete_naming_convention.md`

**Key Rules:**
- **Collections/Endpoints**: PLURAL (`vendors.py`, `domains.py`)
- **Entities/Models**: SINGULAR (`vendor.py`, `domain.py`)
- **Services**: SINGULAR + "service" (`vendor_service.py`, `domain_service.py`)
- **Exceptions**: SINGULAR domain name (`vendor.py`, `domain.py`)

### 3. Identify Dependencies

**Check existing modules:**
- Related database models
- Existing services to reuse
- Exception types needed
- Authentication requirements

---

## Database Layer

### Step 1: Create Database Model

**Location:** `models/database/{entity}.py` (SINGULAR)

**File Naming:** Use singular entity name
- ✅ `models/database/vendor_domain.py`
- ❌ `models/database/vendor_domains.py`

**Template:**

```python
# models/database/vendor_domain.py
"""
VendorDomain database model.

This model represents custom domains for vendors.
"""

from datetime import datetime, timezone
from sqlalchemy import (
    Column, Integer, String, Boolean, DateTime,
    ForeignKey, UniqueConstraint, Index
)
from sqlalchemy.orm import relationship

from app.core.database import Base
from models.database.base import TimestampMixin


class VendorDomain(Base, TimestampMixin):
    """
    Custom domain mapping for vendors.
    
    Allows vendors to use their own domains (e.g., myshop.com)
    instead of subdomains (vendor1.platform.com).
    """
    __tablename__ = "vendor_domains"  # PLURAL table name

    # Primary Key
    id = Column(Integer, primary_key=True, index=True)
    
    # Foreign Keys
    vendor_id = Column(
        Integer, 
        ForeignKey("vendors.id", ondelete="CASCADE"), 
        nullable=False,
        index=True
    )

    # Domain Configuration
    domain = Column(String(255), nullable=False, unique=True, index=True)
    is_primary = Column(Boolean, default=False, nullable=False)
    is_active = Column(Boolean, default=True, nullable=False)
    
    # Verification
    is_verified = Column(Boolean, default=False, nullable=False)
    verification_token = Column(String(100), unique=True, nullable=True)
    verified_at = Column(DateTime(timezone=True), nullable=True)
    
    # SSL Status
    ssl_status = Column(String(50), default="pending")
    ssl_verified_at = Column(DateTime(timezone=True), nullable=True)

    # Relationships
    vendor = relationship("Vendor", back_populates="domains")

    # Constraints and Indexes
    __table_args__ = (
        UniqueConstraint('vendor_id', 'domain', name='uq_vendor_domain'),
        Index('idx_domain_active', 'domain', 'is_active'),
        Index('idx_vendor_primary', 'vendor_id', 'is_primary'),
    )

    def __repr__(self):
        return f"<VendorDomain(id={self.id}, domain='{self.domain}', vendor_id={self.vendor_id})>"

    # Helper Methods
    @classmethod
    def normalize_domain(cls, domain: str) -> str:
        """Normalize domain for consistent storage."""
        domain = domain.replace("https://", "").replace("http://", "")
        domain = domain.rstrip("/")
        domain = domain.lower()
        return domain

    @property
    def full_url(self):
        """Return full URL with https."""
        return f"https://{self.domain}"
```

**Key Points:**
- Use `TimestampMixin` for `created_at`/`updated_at`
- Add proper indexes for query performance
- Include foreign key constraints with `ondelete` behavior
- Use singular class name, plural table name
- Add docstrings for class and complex methods
- Include `__repr__` for debugging

### Step 2: Update Related Models

**Location:** Update the parent model (e.g., `models/database/vendor.py`)

```python
# models/database/vendor.py

class Vendor(Base, TimestampMixin):
    __tablename__ = "vendors"
    
    # ... existing fields ...
    
    # Add relationship
    domains = relationship(
        "VendorDomain",
        back_populates="vendor",
        cascade="all, delete-orphan",
        order_by="VendorDomain.is_primary.desc()"
    )
    
    # Helper method
    @property
    def primary_domain(self):
        """Get the primary custom domain for this vendor."""
        for domain in self.domains:
            if domain.is_primary and domain.is_active:
                return domain.domain
        return None
```

### Step 3: Create Database Migration

```bash
# Create migration
alembic revision --autogenerate -m "Add vendor_domains table"

# Review the generated migration file
# Edit if needed for data migrations or complex changes

# Apply migration
alembic upgrade head
```

**Migration Checklist:**
- [ ] Table created with correct name
- [ ] All columns present with correct types
- [ ] Indexes created
- [ ] Foreign keys configured
- [ ] Unique constraints added
- [ ] Default values set

---

## Schema Layer (Pydantic)

### Step 1: Create Pydantic Schemas

**Location:** `models/schema/{entity}.py` (SINGULAR)

**File Naming:** Use singular entity name
- ✅ `models/schema/vendor_domain.py`
- ❌ `models/schema/vendor_domains.py`

**Template:**

```python
# models/schema/vendor_domain.py
"""
Pydantic schemas for VendorDomain operations.

Schemas include:
- VendorDomainCreate: For adding custom domains
- VendorDomainUpdate: For updating domain settings
- VendorDomainResponse: Standard domain response
- VendorDomainListResponse: Paginated domain list
"""

import re
from datetime import datetime
from typing import List, Optional, Dict
from pydantic import BaseModel, ConfigDict, Field, field_validator


# ============================================================================
# Request Schemas (Input)
# ============================================================================

class VendorDomainCreate(BaseModel):
    """Schema for adding a custom domain to vendor."""

    domain: str = Field(
        ...,
        description="Custom domain (e.g., myshop.com)",
        min_length=3,
        max_length=255,
        examples=["myshop.com", "shop.mybrand.com"]
    )
    is_primary: bool = Field(
        default=False,
        description="Set as primary domain for the vendor"
    )

    @field_validator('domain')
    @classmethod
    def validate_domain(cls, v: str) -> str:
        """Validate and normalize domain."""
        # Remove protocol if present
        domain = v.replace("https://", "").replace("http://", "")
        
        # Remove trailing slash
        domain = domain.rstrip("/")
        
        # Convert to lowercase
        domain = domain.lower().strip()

        # Basic validation
        if not domain or '/' in domain:
            raise ValueError("Invalid domain format")

        if '.' not in domain:
            raise ValueError("Domain must have at least one dot")

        # Check for reserved subdomains
        reserved = ['www', 'admin', 'api', 'mail', 'smtp', 'ftp']
        first_part = domain.split('.')[0]
        if first_part in reserved:
            raise ValueError(f"Domain cannot start with reserved subdomain: {first_part}")

        # Validate domain format (basic regex)
        pattern = r'^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$'
        if not re.match(pattern, domain):
            raise ValueError("Invalid domain format")

        return domain


class VendorDomainUpdate(BaseModel):
    """Schema for updating vendor domain settings."""

    is_primary: Optional[bool] = Field(None, description="Set as primary domain")
    is_active: Optional[bool] = Field(None, description="Activate or deactivate domain")

    model_config = ConfigDict(from_attributes=True)


# ============================================================================
# Response Schemas (Output)
# ============================================================================

class VendorDomainResponse(BaseModel):
    """Standard schema for vendor domain response."""
    model_config = ConfigDict(from_attributes=True)

    id: int
    vendor_id: int
    domain: str
    is_primary: bool
    is_active: bool
    is_verified: bool
    ssl_status: str
    verification_token: Optional[str] = None
    verified_at: Optional[datetime] = None
    ssl_verified_at: Optional[datetime] = None
    created_at: datetime
    updated_at: datetime


class VendorDomainListResponse(BaseModel):
    """Schema for paginated vendor domain list."""
    
    domains: List[VendorDomainResponse]
    total: int


# ============================================================================
# Specialized Schemas
# ============================================================================

class DomainVerificationInstructions(BaseModel):
    """DNS verification instructions for domain ownership."""

    domain: str
    verification_token: str
    instructions: Dict[str, str]
    txt_record: Dict[str, str]
    common_registrars: Dict[str, str]


class DomainVerificationResponse(BaseModel):
    """Response after domain verification."""

    message: str
    domain: str
    verified_at: datetime
    is_verified: bool
```

**Key Points:**
- Separate request schemas (Create, Update) from response schemas
- Use `Field()` for descriptions and examples
- Add validators with `@field_validator`
- Use `ConfigDict(from_attributes=True)` for ORM compatibility
- Document all fields with descriptions
- Use appropriate types (Optional, List, Dict)

---

## Exception Layer

### Step 1: Create Custom Exceptions

**Location:** `app/exceptions/{domain}.py` (SINGULAR)

**File Naming:** Use singular domain name
- ✅ `app/exceptions/vendor_domain.py`
- ❌ `app/exceptions/vendor_domains.py`

**Template:**

```python
# app/exceptions/vendor_domain.py
"""
Vendor domain management specific exceptions.

All exceptions follow the pattern:
- Inherit from appropriate base exception
- Include relevant context in details dict
- Use proper HTTP status codes
- Provide clear, actionable error messages
"""

from typing import Any, Dict, Optional
from .base import (
    ResourceNotFoundException,
    ConflictException,
    ValidationException,
    BusinessLogicException,
    ExternalServiceException
)


# ============================================================================
# Resource Not Found (404)
# ============================================================================

class VendorDomainNotFoundException(ResourceNotFoundException):
    """Raised when a vendor domain is not found."""

    def __init__(self, domain_identifier: str, identifier_type: str = "ID"):
        if identifier_type.lower() == "domain":
            message = f"Domain '{domain_identifier}' not found"
        else:
            message = f"Domain with ID '{domain_identifier}' not found"

        super().__init__(
            resource_type="VendorDomain",
            identifier=domain_identifier,
            message=message,
            error_code="VENDOR_DOMAIN_NOT_FOUND",
        )


# ============================================================================
# Conflicts (409)
# ============================================================================

class VendorDomainAlreadyExistsException(ConflictException):
    """Raised when trying to add a domain that already exists."""

    def __init__(self, domain: str, existing_vendor_id: Optional[int] = None):
        details = {"domain": domain}
        if existing_vendor_id:
            details["existing_vendor_id"] = existing_vendor_id

        super().__init__(
            message=f"Domain '{domain}' is already registered",
            error_code="VENDOR_DOMAIN_ALREADY_EXISTS",
            details=details,
        )


# ============================================================================
# Validation Errors (422)
# ============================================================================

class InvalidDomainFormatException(ValidationException):
    """Raised when domain format is invalid."""

    def __init__(self, domain: str, reason: str = "Invalid domain format"):
        super().__init__(
            message=f"{reason}: {domain}",
            field="domain",
            details={"domain": domain, "reason": reason},
        )
        self.error_code = "INVALID_DOMAIN_FORMAT"


class ReservedDomainException(ValidationException):
    """Raised when trying to use a reserved domain."""

    def __init__(self, domain: str, reserved_part: str):
        super().__init__(
            message=f"Domain cannot use reserved subdomain: {reserved_part}",
            field="domain",
            details={"domain": domain, "reserved_part": reserved_part},
        )
        self.error_code = "RESERVED_DOMAIN"


# ============================================================================
# Business Logic Errors (400)
# ============================================================================

class DomainNotVerifiedException(BusinessLogicException):
    """Raised when trying to activate an unverified domain."""

    def __init__(self, domain_id: int, domain: str):
        super().__init__(
            message=f"Domain '{domain}' must be verified before activation",
            error_code="DOMAIN_NOT_VERIFIED",
            details={"domain_id": domain_id, "domain": domain},
        )


class DomainVerificationFailedException(BusinessLogicException):
    """Raised when domain verification fails."""

    def __init__(self, domain: str, reason: str):
        super().__init__(
            message=f"Domain verification failed for '{domain}': {reason}",
            error_code="DOMAIN_VERIFICATION_FAILED",
            details={"domain": domain, "reason": reason},
        )


class MaxDomainsReachedException(BusinessLogicException):
    """Raised when vendor tries to add more domains than allowed."""

    def __init__(self, vendor_id: int, max_domains: int):
        super().__init__(
            message=f"Maximum number of domains reached ({max_domains})",
            error_code="MAX_DOMAINS_REACHED",
            details={"vendor_id": vendor_id, "max_domains": max_domains},
        )


# ============================================================================
# External Service Errors (502)
# ============================================================================

class DNSVerificationException(ExternalServiceException):
    """Raised when DNS verification service fails."""

    def __init__(self, domain: str, reason: str):
        super().__init__(
            service_name="DNS",
            message=f"DNS verification failed for '{domain}': {reason}",
            error_code="DNS_VERIFICATION_ERROR",
            details={"domain": domain, "reason": reason},
        )
```

**Exception Categories:**

| HTTP Status | Base Exception | Use Case |
|-------------|----------------|----------|
| 400 | `BusinessLogicException` | Business rule violations |
| 401 | `AuthenticationException` | Authentication failures |
| 403 | `AuthorizationException` | Permission denied |
| 404 | `ResourceNotFoundException` | Resource not found |
| 409 | `ConflictException` | Resource conflicts |
| 422 | `ValidationException` | Input validation errors |
| 429 | `RateLimitException` | Rate limiting |
| 500 | `WizamartException` | Generic errors |
| 502 | `ExternalServiceException` | Third-party failures |

### Step 2: Update Exception Exports

**Location:** `app/exceptions/__init__.py`

```python
# app/exceptions/__init__.py

# ... existing imports ...

# Vendor domain exceptions
from .vendor_domain import (
    VendorDomainNotFoundException,
    VendorDomainAlreadyExistsException,
    InvalidDomainFormatException,
    ReservedDomainException,
    DomainNotVerifiedException,
    DomainVerificationFailedException,
    MaxDomainsReachedException,
    DNSVerificationException,
)

__all__ = [
    # ... existing exports ...
    
    # Vendor Domain
    "VendorDomainNotFoundException",
    "VendorDomainAlreadyExistsException",
    "InvalidDomainFormatException",
    "ReservedDomainException",
    "DomainNotVerifiedException",
    "DomainVerificationFailedException",
    "MaxDomainsReachedException",
    "DNSVerificationException",
]
```

---

## Service Layer

### Step 1: Create Service Class

**Location:** `app/services/{entity}_service.py` (SINGULAR + service)

**File Naming:** Use singular entity name + "_service"
- ✅ `app/services/vendor_domain_service.py`
- ❌ `app/services/vendor_domains_service.py`

**Template:**

```python
# app/services/vendor_domain_service.py
"""
Vendor domain service for managing custom domain operations.

This module provides classes and functions for:
- Adding and removing custom domains
- Domain verification via DNS
- Domain activation and deactivation
- Setting primary domains
- Domain validation and normalization
"""

import logging
import secrets
from typing import List, Tuple, Optional
from datetime import datetime, timezone

from sqlalchemy.orm import Session

from app.exceptions import (
    VendorNotFoundException,
    VendorDomainNotFoundException,
    VendorDomainAlreadyExistsException,
    InvalidDomainFormatException,
    DomainNotVerifiedException,
    DomainVerificationFailedException,
    MaxDomainsReachedException,
    ValidationException,
)
from models.schema.vendor_domain import VendorDomainCreate, VendorDomainUpdate
from models.database.vendor import Vendor
from models.database.vendor_domain import VendorDomain

logger = logging.getLogger(__name__)


class VendorDomainService:
    """Service class for vendor domain operations."""

    def __init__(self):
        """Initialize service with configuration."""
        self.max_domains_per_vendor = 10
        self.reserved_subdomains = ['www', 'admin', 'api', 'mail', 'smtp', 'ftp']

    def add_domain(
        self,
        db: Session,
        vendor_id: int,
        domain_data: VendorDomainCreate
    ) -> VendorDomain:
        """
        Add a custom domain to vendor.

        Args:
            db: Database session
            vendor_id: Vendor ID to add domain to
            domain_data: Domain creation data

        Returns:
            Created VendorDomain object

        Raises:
            VendorNotFoundException: If vendor not found
            VendorDomainAlreadyExistsException: If domain already registered
            MaxDomainsReachedException: If vendor has reached max domains
            InvalidDomainFormatException: If domain format is invalid
        """
        try:
            # 1. Verify vendor exists
            vendor = self._get_vendor_by_id_or_raise(db, vendor_id)

            # 2. Check domain limit
            self._check_domain_limit(db, vendor_id)

            # 3. Normalize domain
            normalized_domain = VendorDomain.normalize_domain(domain_data.domain)

            # 4. Validate domain format
            self._validate_domain_format(normalized_domain)

            # 5. Check if domain already exists
            if self._domain_exists(db, normalized_domain):
                existing = db.query(VendorDomain).filter(
                    VendorDomain.domain == normalized_domain
                ).first()
                raise VendorDomainAlreadyExistsException(
                    normalized_domain,
                    existing.vendor_id if existing else None
                )

            # 6. If setting as primary, unset other primary domains
            if domain_data.is_primary:
                self._unset_primary_domains(db, vendor_id)

            # 7. Create domain record
            new_domain = VendorDomain(
                vendor_id=vendor_id,
                domain=normalized_domain,
                is_primary=domain_data.is_primary,
                verification_token=secrets.token_urlsafe(32),
                is_verified=False,
                is_active=False,
                ssl_status="pending"
            )

            db.add(new_domain)
            db.commit()
            db.refresh(new_domain)

            logger.info(f"Domain {normalized_domain} added to vendor {vendor_id}")
            return new_domain

        except (
            VendorNotFoundException,
            VendorDomainAlreadyExistsException,
            MaxDomainsReachedException,
            InvalidDomainFormatException
        ):
            db.rollback()
            raise
        except Exception as e:
            db.rollback()
            logger.error(f"Error adding domain: {str(e)}")
            raise ValidationException("Failed to add domain")

    def get_vendor_domains(
        self,
        db: Session,
        vendor_id: int
    ) -> List[VendorDomain]:
        """
        Get all domains for a vendor.

        Args:
            db: Database session
            vendor_id: Vendor ID

        Returns:
            List of VendorDomain objects

        Raises:
            VendorNotFoundException: If vendor not found
        """
        try:
            # Verify vendor exists
            self._get_vendor_by_id_or_raise(db, vendor_id)

            domains = db.query(VendorDomain).filter(
                VendorDomain.vendor_id == vendor_id
            ).order_by(
                VendorDomain.is_primary.desc(),
                VendorDomain.created_at.desc()
            ).all()

            return domains

        except VendorNotFoundException:
            raise
        except Exception as e:
            logger.error(f"Error getting vendor domains: {str(e)}")
            raise ValidationException("Failed to retrieve domains")

    def update_domain(
        self,
        db: Session,
        domain_id: int,
        domain_update: VendorDomainUpdate
    ) -> VendorDomain:
        """
        Update domain settings.

        Args:
            db: Database session
            domain_id: Domain ID
            domain_update: Update data

        Returns:
            Updated VendorDomain object

        Raises:
            VendorDomainNotFoundException: If domain not found
            DomainNotVerifiedException: If trying to activate unverified domain
        """
        try:
            domain = self._get_domain_by_id_or_raise(db, domain_id)

            # If setting as primary, unset other primary domains
            if domain_update.is_primary:
                self._unset_primary_domains(db, domain.vendor_id, exclude_domain_id=domain_id)
                domain.is_primary = True

            # If activating, check verification
            if domain_update.is_active is True and not domain.is_verified:
                raise DomainNotVerifiedException(domain_id, domain.domain)

            # Update fields
            if domain_update.is_active is not None:
                domain.is_active = domain_update.is_active

            db.commit()
            db.refresh(domain)

            logger.info(f"Domain {domain.domain} updated")
            return domain

        except (VendorDomainNotFoundException, DomainNotVerifiedException):
            db.rollback()
            raise
        except Exception as e:
            db.rollback()
            logger.error(f"Error updating domain: {str(e)}")
            raise ValidationException("Failed to update domain")

    def delete_domain(
        self,
        db: Session,
        domain_id: int
    ) -> str:
        """
        Delete a custom domain.

        Args:
            db: Database session
            domain_id: Domain ID

        Returns:
            Success message

        Raises:
            VendorDomainNotFoundException: If domain not found
        """
        try:
            domain = self._get_domain_by_id_or_raise(db, domain_id)
            domain_name = domain.domain
            vendor_id = domain.vendor_id

            db.delete(domain)
            db.commit()

            logger.info(f"Domain {domain_name} deleted from vendor {vendor_id}")
            return f"Domain {domain_name} deleted successfully"

        except VendorDomainNotFoundException:
            db.rollback()
            raise
        except Exception as e:
            db.rollback()
            logger.error(f"Error deleting domain: {str(e)}")
            raise ValidationException("Failed to delete domain")

    # ========================================================================
    # Private Helper Methods (use _ prefix)
    # ========================================================================

    def _get_vendor_by_id_or_raise(self, db: Session, vendor_id: int) -> Vendor:
        """Get vendor by ID or raise exception."""
        vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
        if not vendor:
            raise VendorNotFoundException(str(vendor_id), identifier_type="id")
        return vendor

    def _get_domain_by_id_or_raise(self, db: Session, domain_id: int) -> VendorDomain:
        """Get domain by ID or raise exception."""
        domain = db.query(VendorDomain).filter(VendorDomain.id == domain_id).first()
        if not domain:
            raise VendorDomainNotFoundException(str(domain_id))
        return domain

    def _check_domain_limit(self, db: Session, vendor_id: int) -> None:
        """Check if vendor has reached maximum domain limit."""
        domain_count = db.query(VendorDomain).filter(
            VendorDomain.vendor_id == vendor_id
        ).count()

        if domain_count >= self.max_domains_per_vendor:
            raise MaxDomainsReachedException(vendor_id, self.max_domains_per_vendor)

    def _domain_exists(self, db: Session, domain: str) -> bool:
        """Check if domain already exists in system."""
        return db.query(VendorDomain).filter(
            VendorDomain.domain == domain
        ).first() is not None

    def _validate_domain_format(self, domain: str) -> None:
        """Validate domain format and check for reserved subdomains."""
        first_part = domain.split('.')[0]
        if first_part in self.reserved_subdomains:
            from app.exceptions import ReservedDomainException
            raise ReservedDomainException(domain, first_part)

    def _unset_primary_domains(
        self,
        db: Session,
        vendor_id: int,
        exclude_domain_id: Optional[int] = None
    ) -> None:
        """Unset all primary domains for vendor."""
        query = db.query(VendorDomain).filter(
            VendorDomain.vendor_id == vendor_id,
            VendorDomain.is_primary == True
        )

        if exclude_domain_id:
            query = query.filter(VendorDomain.id != exclude_domain_id)

        query.update({"is_primary": False})


# Create service instance
vendor_domain_service = VendorDomainService()
```

**Service Layer Best Practices:**
- ✅ All business logic in service layer
- ✅ Raise custom exceptions (not HTTPException)
- ✅ Transaction management (commit/rollback)
- ✅ Comprehensive logging
- ✅ Helper methods with `_` prefix
- ✅ Detailed docstrings
- ✅ Type hints for all methods
- ✅ Create singleton instance at bottom

---

## API Layer (Endpoints)

### Step 1: Create API Endpoints

**Location:** `app/api/v1/admin/{entities}.py` (PLURAL)

**File Naming:** Use plural entity name
- ✅ `app/api/v1/admin/vendor_domains.py`
- ❌ `app/api/v1/admin/vendor_domain.py`

**Template:**

```python
# app/api/v1/admin/vendor_domains.py
"""
Admin endpoints for managing vendor custom domains.

Follows the architecture pattern:
- Endpoints only handle HTTP layer
- Business logic in service layer
- Proper exception handling
- Pydantic schemas for validation
"""

import logging
from typing import List

from fastapi import APIRouter, Depends, Path, Body, Query
from sqlalchemy.orm import Session

from app.api.deps import get_current_admin_api, get_db
from app.services.vendor_domain_service import vendor_domain_service
from app.exceptions import VendorNotFoundException
from models.schema.vendor_domain import (
    VendorDomainCreate,
    VendorDomainUpdate,
    VendorDomainResponse,
    VendorDomainListResponse,
)
from models.database.user import User
from models.database.vendor import Vendor

router = APIRouter(prefix="/vendors")
logger = logging.getLogger(__name__)


# ============================================================================
# Helper Functions
# ============================================================================

def _get_vendor_by_id(db: Session, vendor_id: int) -> Vendor:
    """
    Helper to get vendor by ID.

    Args:
        db: Database session
        vendor_id: Vendor ID

    Returns:
        Vendor object

    Raises:
        VendorNotFoundException: If vendor not found
    """
    vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
    if not vendor:
        raise VendorNotFoundException(str(vendor_id), identifier_type="id")
    return vendor


# ============================================================================
# Endpoints
# ============================================================================

@router.post("/{vendor_id}/domains", response_model=VendorDomainResponse)
def add_vendor_domain(
    vendor_id: int = Path(..., description="Vendor ID", gt=0),
    domain_data: VendorDomainCreate = Body(...),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """
    Add a custom domain to vendor (Admin only).

    This endpoint:
    1. Validates the domain format
    2. Checks if domain is already registered
    3. Generates verification token
    4. Creates domain record (unverified, inactive)
    5. Returns domain with verification instructions

    **Domain Examples:**
    - myshop.com
    - shop.mybrand.com
    - customstore.net

    **Raises:**
    - 404: Vendor not found
    - 409: Domain already registered
    - 422: Invalid domain format or reserved subdomain
    """
    domain = vendor_domain_service.add_domain(
        db=db,
        vendor_id=vendor_id,
        domain_data=domain_data
    )

    return VendorDomainResponse(
        id=domain.id,
        vendor_id=domain.vendor_id,
        domain=domain.domain,
        is_primary=domain.is_primary,
        is_active=domain.is_active,
        is_verified=domain.is_verified,
        ssl_status=domain.ssl_status,
        verification_token=domain.verification_token,
        verified_at=domain.verified_at,
        ssl_verified_at=domain.ssl_verified_at,
        created_at=domain.created_at,
        updated_at=domain.updated_at,
    )


@router.get("/{vendor_id}/domains", response_model=VendorDomainListResponse)
def list_vendor_domains(
    vendor_id: int = Path(..., description="Vendor ID", gt=0),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """
    List all domains for a vendor (Admin only).

    Returns domains ordered by:
    1. Primary domains first
    2. Creation date (newest first)

    **Raises:**
    - 404: Vendor not found
    """
    # Verify vendor exists
    _get_vendor_by_id(db, vendor_id)

    domains = vendor_domain_service.get_vendor_domains(db, vendor_id)

    return VendorDomainListResponse(
        domains=[
            VendorDomainResponse(
                id=d.id,
                vendor_id=d.vendor_id,
                domain=d.domain,
                is_primary=d.is_primary,
                is_active=d.is_active,
                is_verified=d.is_verified,
                ssl_status=d.ssl_status,
                verification_token=d.verification_token if not d.is_verified else None,
                verified_at=d.verified_at,
                ssl_verified_at=d.ssl_verified_at,
                created_at=d.created_at,
                updated_at=d.updated_at,
            )
            for d in domains
        ],
        total=len(domains)
    )


@router.put("/domains/{domain_id}", response_model=VendorDomainResponse)
def update_vendor_domain(
    domain_id: int = Path(..., description="Domain ID", gt=0),
    domain_update: VendorDomainUpdate = Body(...),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """
    Update domain settings (Admin only).

    **Can update:**
    - `is_primary`: Set as primary domain for vendor
    - `is_active`: Activate or deactivate domain

    **Important:**
    - Cannot activate unverified domains
    - Setting a domain as primary will unset other primary domains

    **Raises:**
    - 404: Domain not found
    - 400: Cannot activate unverified domain
    """
    domain = vendor_domain_service.update_domain(
        db=db,
        domain_id=domain_id,
        domain_update=domain_update
    )

    return VendorDomainResponse(
        id=domain.id,
        vendor_id=domain.vendor_id,
        domain=domain.domain,
        is_primary=domain.is_primary,
        is_active=domain.is_active,
        is_verified=domain.is_verified,
        ssl_status=domain.ssl_status,
        verification_token=None,
        verified_at=domain.verified_at,
        ssl_verified_at=domain.ssl_verified_at,
        created_at=domain.created_at,
        updated_at=domain.updated_at,
    )


@router.delete("/domains/{domain_id}")
def delete_vendor_domain(
    domain_id: int = Path(..., description="Domain ID", gt=0),
    db: Session = Depends(get_db),
    current_admin: User = Depends(get_current_admin_api),
):
    """
    Delete a custom domain (Admin only).

    **Warning:** This is permanent and cannot be undone.
    
    **Raises:**
    - 404: Domain not found
    """
    message = vendor_domain_service.delete_domain(db, domain_id)
    return {"message": message}
```

**Endpoint Best Practices:**
- ✅ Use plural file names for collections
- ✅ Prefix for related resources (`/vendors/{id}/domains`)
- ✅ Path parameters with validation (`gt=0`)
- ✅ Comprehensive docstrings with examples
- ✅ Delegate to service layer immediately
- ✅ No business logic in endpoints
- ✅ Proper dependency injection
- ✅ Response models for consistency

### Step 2: Register Router

**Location:** `app/api/v1/admin/__init__.py`

```python
# app/api/v1/admin/__init__.py

from fastapi import APIRouter
from . import (
    auth,
    vendors,
    vendor_domains,  # NEW
    users,
    dashboard,
    # ... other routers
)

router = APIRouter()

# Include routers
router.include_router(auth.router, tags=["admin-auth"])
router.include_router(vendors.router, tags=["admin-vendors"])
router.include_router(vendor_domains.router, tags=["admin-vendor-domains"])  # NEW
router.include_router(users.router, tags=["admin-users"])
# ... other routers
```

---

## Frontend Layer

### Step 1: Create HTML Page Route

**Location:** `app/api/v1/admin/pages.py`

```python
# app/api/v1/admin/pages.py

@router.get("/vendors/{vendor_code}/domains", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_domains_page(
        request: Request,
        vendor_code: str = Path(..., description="Vendor code"),
        current_user: User = Depends(get_current_admin_from_cookie_or_header),
        db: Session = Depends(get_db)
):
    """
    Render vendor domains management page.
    Shows custom domains, verification status, and DNS configuration.
    """
    return templates.TemplateResponse(
        "admin/vendor-domains.html",
        {
            "request": request,
            "user": current_user,
            "vendor_code": vendor_code,
        }
    )
```

### Step 2: Create HTML Template

**Location:** `app/templates/admin/vendor-domains.html`

```html
{% extends "admin/base.html" %}

{% block title %}Vendor Domains - {{ vendor_code }}{% endblock %}

{% block content %}
<div class="container mx-auto px-6 py-8">
    <div class="flex items-center justify-between mb-6">
        <div>
            <h1 class="text-3xl font-semibold text-gray-800 dark:text-white">
                Custom Domains
            </h1>
            <p class="text-gray-600 dark:text-gray-400 mt-2">
                Manage custom domains for {{ vendor_code }}
            </p>
        </div>
        <button 
            onclick="openAddDomainModal()"
            class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
            Add Domain
        </button>
    </div>

    <!-- Domains List -->
    <div id="domainsList" class="space-y-4">
        <!-- Dynamically loaded via JavaScript -->
    </div>
</div>

<!-- Add Domain Modal -->
<div id="addDomainModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50">
    <div class="flex items-center justify-center min-h-screen px-4">
        <div class="bg-white dark:bg-gray-800 rounded-lg max-w-md w-full p-6">
            <h2 class="text-2xl font-semibold mb-4">Add Custom Domain</h2>
            
            <form id="addDomainForm">
                <div class="mb-4">
                    <label class="block text-sm font-medium mb-2">Domain</label>
                    <input 
                        type="text" 
                        name="domain"
                        placeholder="myshop.com"
                        class="w-full px-3 py-2 border rounded-lg"
                        required>
                </div>
                
                <div class="mb-4">
                    <label class="flex items-center">
                        <input type="checkbox" name="is_primary" class="mr-2">
                        <span class="text-sm">Set as primary domain</span>
                    </label>
                </div>
                
                <div class="flex gap-2">
                    <button 
                        type="submit"
                        class="flex-1 bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700">
                        Add Domain
                    </button>
                    <button 
                        type="button"
                        onclick="closeAddDomainModal()"
                        class="flex-1 bg-gray-300 text-gray-700 py-2 rounded-lg hover:bg-gray-400">
                        Cancel
                    </button>
                </div>
            </form>
        </div>
    </div>
</div>

<script>
// Get vendor ID from URL
const vendorCode = "{{ vendor_code }}";
let vendorId = null;

// Load domains on page load
document.addEventListener('DOMContentLoaded', async () => {
    // First, get vendor ID from vendor code
    await loadVendorId();
    // Then load domains
    await loadDomains();
});

async function loadVendorId() {
    try {
        const response = await fetch(`/api/v1/admin/vendors/${vendorCode}`, {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        if (response.ok) {
            const vendor = await response.json();
            vendorId = vendor.id;
        }
    } catch (error) {
        console.error('Error loading vendor:', error);
    }
}

async function loadDomains() {
    if (!vendorId) return;
    
    try {
        const response = await fetch(`/api/v1/admin/vendors/${vendorId}/domains`, {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        if (response.ok) {
            const data = await response.json();
            renderDomains(data.domains);
        }
    } catch (error) {
        console.error('Error loading domains:', error);
    }
}

function renderDomains(domains) {
    const container = document.getElementById('domainsList');
    
    if (domains.length === 0) {
        container.innerHTML = '<p class="text-gray-500">No domains added yet.</p>';
        return;
    }
    
    container.innerHTML = domains.map(domain => `
        <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
            <div class="flex items-center justify-between">
                <div class="flex-1">
                    <div class="flex items-center gap-2">
                        <h3 class="text-lg font-semibold">${domain.domain}</h3>
                        ${domain.is_primary ? '<span class="px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded">Primary</span>' : ''}
                        ${domain.is_verified ? '<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded">Verified</span>' : '<span class="px-2 py-1 bg-yellow-100 text-yellow-800 text-xs rounded">Unverified</span>'}
                        ${domain.is_active ? '<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">Active</span>' : '<span class="px-2 py-1 bg-gray-100 text-gray-800 text-xs rounded">Inactive</span>'}
                    </div>
                    <p class="text-sm text-gray-600 dark:text-gray-400 mt-1">
                        Added ${new Date(domain.created_at).toLocaleDateString()}
                    </p>
                </div>
                <div class="flex gap-2">
                    ${!domain.is_verified ? `
                        <button 
                            onclick="verifyDomain(${domain.id})"
                            class="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
                            Verify
                        </button>
                    ` : ''}
                    <button 
                        onclick="deleteDomain(${domain.id})"
                        class="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700">
                        Delete
                    </button>
                </div>
            </div>
        </div>
    `).join('');
}

function openAddDomainModal() {
    document.getElementById('addDomainModal').classList.remove('hidden');
}

function closeAddDomainModal() {
    document.getElementById('addDomainModal').classList.add('hidden');
    document.getElementById('addDomainForm').reset();
}

document.getElementById('addDomainForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    const data = {
        domain: formData.get('domain'),
        is_primary: formData.get('is_primary') === 'on'
    };
    
    try {
        const response = await fetch(`/api/v1/admin/vendors/${vendorId}/domains`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            },
            body: JSON.stringify(data)
        });
        
        if (response.ok) {
            closeAddDomainModal();
            await loadDomains();
            alert('Domain added successfully!');
        } else {
            const error = await response.json();
            alert(`Error: ${error.message}`);
        }
    } catch (error) {
        console.error('Error adding domain:', error);
        alert('Failed to add domain');
    }
});

async function verifyDomain(domainId) {
    try {
        const response = await fetch(`/api/v1/admin/vendors/domains/${domainId}/verify`, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        if (response.ok) {
            await loadDomains();
            alert('Domain verified successfully!');
        } else {
            const error = await response.json();
            alert(`Verification failed: ${error.message}`);
        }
    } catch (error) {
        console.error('Error verifying domain:', error);
        alert('Failed to verify domain');
    }
}

async function deleteDomain(domainId) {
    if (!confirm('Are you sure you want to delete this domain?')) return;
    
    try {
        const response = await fetch(`/api/v1/admin/vendors/domains/${domainId}`, {
            method: 'DELETE',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        if (response.ok) {
            await loadDomains();
            alert('Domain deleted successfully!');
        } else {
            const error = await response.json();
            alert(`Error: ${error.message}`);
        }
    } catch (error) {
        console.error('Error deleting domain:', error);
        alert('Failed to delete domain');
    }
}
</script>
{% endblock %}
```

---

## Testing

### Step 1: Unit Tests (Service Layer)

**Location:** `tests/unit/services/test_vendor_domain_service.py`

```python
# tests/unit/services/test_vendor_domain_service.py
import pytest
from app.services.vendor_domain_service import VendorDomainService
from app.exceptions import (
    VendorDomainAlreadyExistsException,
    MaxDomainsReachedException,
    InvalidDomainFormatException
)
from models.schema.vendor_domain import VendorDomainCreate


@pytest.fixture
def service():
    return VendorDomainService()


def test_add_domain_success(db_session, test_vendor, service):
    """Test successful domain addition."""
    domain_data = VendorDomainCreate(
        domain="test.com",
        is_primary=True
    )
    
    domain = service.add_domain(db_session, test_vendor.id, domain_data)
    
    assert domain.domain == "test.com"
    assert domain.is_primary is True
    assert domain.is_verified is False
    assert domain.verification_token is not None


def test_add_domain_already_exists(db_session, test_vendor, existing_domain, service):
    """Test adding duplicate domain raises exception."""
    domain_data = VendorDomainCreate(domain=existing_domain.domain)
    
    with pytest.raises(VendorDomainAlreadyExistsException):
        service.add_domain(db_session, test_vendor.id, domain_data)


def test_add_domain_max_limit_reached(db_session, test_vendor, service):
    """Test max domains limit enforcement."""
    # Add max_domains domains
    for i in range(service.max_domains_per_vendor):
        domain_data = VendorDomainCreate(domain=f"test{i}.com")
        service.add_domain(db_session, test_vendor.id, domain_data)
    
    # Try adding one more
    domain_data = VendorDomainCreate(domain="overflow.com")
    with pytest.raises(MaxDomainsReachedException):
        service.add_domain(db_session, test_vendor.id, domain_data)


def test_domain_normalization(service):
    """Test domain normalization."""
    assert service._normalize_domain("HTTP://TEST.COM/") == "test.com"
    assert service._normalize_domain("WWW.Test.COM") == "www.test.com"
```

### Step 2: Integration Tests (API Endpoints)

**Location:** `tests/integration/api/v1/admin/test_vendor_domains.py`

```python
# tests/integration/api/v1/admin/test_vendor_domains.py
import pytest


def test_add_domain_endpoint(client, admin_headers, test_vendor):
    """Test domain addition endpoint."""
    response = client.post(
        f"/api/v1/admin/vendors/{test_vendor.id}/domains",
        json={"domain": "newshop.com", "is_primary": False},
        headers=admin_headers
    )
    
    assert response.status_code == 201
    data = response.json()
    assert data["domain"] == "newshop.com"
    assert data["is_verified"] is False


def test_add_domain_invalid_format(client, admin_headers, test_vendor):
    """Test adding domain with invalid format."""
    response = client.post(
        f"/api/v1/admin/vendors/{test_vendor.id}/domains",
        json={"domain": "admin.example.com"},  # Reserved subdomain
        headers=admin_headers
    )
    
    assert response.status_code == 422
    assert "reserved" in response.json()["message"].lower()


def test_list_domains_endpoint(client, admin_headers, test_vendor, test_domain):
    """Test listing vendor domains."""
    response = client.get(
        f"/api/v1/admin/vendors/{test_vendor.id}/domains",
        headers=admin_headers
    )
    
    assert response.status_code == 200
    data = response.json()
    assert data["total"] >= 1
    assert len(data["domains"]) >= 1


def test_delete_domain_endpoint(client, admin_headers, test_domain):
    """Test domain deletion."""
    response = client.delete(
        f"/api/v1/admin/vendors/domains/{test_domain.id}",
        headers=admin_headers
    )
    
    assert response.status_code == 200
    assert "deleted successfully" in response.json()["message"]


def test_verify_domain_not_found(client, admin_headers):
    """Test verification with non-existent domain."""
    response = client.post(
        "/api/v1/admin/vendors/domains/99999/verify",
        headers=admin_headers
    )
    
    assert response.status_code == 404
    assert response.json()["error_code"] == "VENDOR_DOMAIN_NOT_FOUND"
```

---

## Documentation

### Step 1: Update API Documentation

**Location:** `docs/api/admin_vendor_domains.md`

```markdown
# Vendor Domains API

## Overview

Custom domain management for vendors.

## Endpoints

### Add Domain
\`\`\`
POST /api/v1/admin/vendors/{vendor_id}/domains
\`\`\`

**Request:**
\`\`\`json
{
  "domain": "myshop.com",
  "is_primary": true
}
\`\`\`

**Response:** `201 Created`
\`\`\`json
{
  "id": 1,
  "domain": "myshop.com",
  "is_verified": false,
  "verification_token": "abc123..."
}
\`\`\`

### List Domains
\`\`\`
GET /api/v1/admin/vendors/{vendor_id}/domains
\`\`\`

### Verify Domain
\`\`\`
POST /api/v1/admin/vendors/domains/{domain_id}/verify
\`\`\`

### Delete Domain
\`\`\`
DELETE /api/v1/admin/vendors/domains/{domain_id}
\`\`\`
```

### Step 2: Update Changelog

**Location:** `CHANGELOG.md`

```markdown
## [Unreleased]

### Added
- Custom domain support for vendors
- DNS verification system
- Domain management UI in admin panel

### API Changes
- Added `/api/v1/admin/vendors/{id}/domains` endpoints
- Added domain verification endpoints
```

---

## Deployment Checklist

### Pre-Deployment

- [ ] All unit tests passing
- [ ] All integration tests passing
- [ ] Database migration created and tested
- [ ] Exception handling tested
- [ ] API documentation updated
- [ ] Frontend UI tested
- [ ] Code review completed
- [ ] Changelog updated

### Deployment Steps

1. **Database Migration**
   ```bash
   alembic upgrade head
   ```

2. **Verify Migration**
   ```bash
   # Check table exists
   SELECT * FROM vendor_domains LIMIT 1;
   ```

3. **Deploy Code**
   ```bash
   git push origin main
   # Deploy via your CI/CD pipeline
   ```

4. **Verify Deployment**
   ```bash
   # Test API endpoint
   curl -X GET https://your-domain.com/api/v1/admin/vendors/1/domains \
     -H "Authorization: Bearer TOKEN"
   ```

5. **Monitor Logs**
   ```bash
   # Check for errors
   tail -f /var/log/app.log | grep ERROR
   ```

### Post-Deployment

- [ ] API endpoints accessible
- [ ] Frontend pages loading
- [ ] Database queries performing well
- [ ] No error logs
- [ ] User acceptance testing completed
- [ ] Documentation deployed

---

## Quick Reference

### File Locations Checklist

```
✅ models/database/vendor_domain.py        # Database model
✅ models/schema/vendor_domain.py          # Pydantic schemas
✅ app/exceptions/vendor_domain.py         # Custom exceptions
✅ app/services/vendor_domain_service.py   # Business logic
✅ app/api/v1/admin/vendor_domains.py      # API endpoints
✅ app/api/v1/admin/pages.py               # HTML routes
✅ app/templates/admin/vendor-domains.html # HTML template
✅ tests/unit/services/test_vendor_domain_service.py
✅ tests/integration/api/v1/admin/test_vendor_domains.py
```

### Naming Convention Summary

| Type | Naming | Example |
|------|--------|---------|
| API File | PLURAL | `vendor_domains.py` |
| Model File | SINGULAR | `vendor_domain.py` |
| Schema File | SINGULAR | `vendor_domain.py` |
| Service File | SINGULAR + service | `vendor_domain_service.py` |
| Exception File | SINGULAR | `vendor_domain.py` |
| Class Name | SINGULAR | `VendorDomain` |
| Table Name | PLURAL | `vendor_domains` |

### Common Pitfalls to Avoid

❌ **Don't:** Put business logic in endpoints
✅ **Do:** Put business logic in service layer

❌ **Don't:** Raise HTTPException from services
✅ **Do:** Raise custom exceptions

❌ **Don't:** Access database directly in endpoints
✅ **Do:** Call service methods

❌ **Don't:** Use plural for model files
✅ **Do:** Use singular for model files

❌ **Don't:** Skip validation
✅ **Do:** Use Pydantic validators

❌ **Don't:** Forget transaction management
✅ **Do:** Use try/except with commit/rollback

---

## Support

For questions or issues:
1. Check this guide
2. Review `exception-handling.md`
3. Reference `6_complete_naming_convention.md`
4. Review existing implementations (vendors, users)

---

**Last Updated:** 2025-01-15  
**Version:** 1.0  
**Maintainer:** Development Team
