import re

from django import forms
from django.http import JsonResponse

from api.views import DOMAIN_API_MESSAGES, check_domain_available
from registrar.utility import errors
from epplibwrapper.errors import RegistryError
from registrar.utility.enums import ValidationReturnType


class DomainHelper:
    """Utility functions and constants for domain names."""

    # a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
    # begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
    DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")

    # a domain can be no longer than 253 characters in total
    MAX_LENGTH = 253

    @classmethod
    def string_could_be_domain(cls, domain: str | None) -> bool:
        """Return True if the string could be a domain name, otherwise False."""
        if not isinstance(domain, str):
            return False
        return bool(cls.DOMAIN_REGEX.match(domain))

    @classmethod
    def validate(cls, domain: str, blank_ok=False) -> str:
        """Attempt to determine if a domain name could be requested."""

        # Split into pieces for the linter
        domain = cls._validate_domain_string(domain, blank_ok)

        try:
            if not check_domain_available(domain):
                raise errors.DomainUnavailableError()
        except RegistryError as err:
            raise errors.RegistrySystemError() from err
        return domain

    @staticmethod
    def _validate_domain_string(domain, blank_ok):
        """Normalize the domain string, and check its content"""
        if domain is None:
            raise errors.BlankValueError()

        if not isinstance(domain, str):
            raise errors.InvalidDomainError()

        domain = domain.lower().strip()

        if domain == "" and not blank_ok:
            raise errors.BlankValueError()
        elif domain == "":
            # If blank ok is true, just return the domain
            return domain

        if domain.endswith(".gov"):
            domain = domain[:-4]

        if "." in domain:
            raise errors.ExtraDotsError()

        if not DomainHelper.string_could_be_domain(domain + ".gov"):
            raise errors.InvalidDomainError()

        return domain

    @classmethod
    def validate_and_handle_errors(cls, domain, return_type, blank_ok=False):
        """
        Validates a domain and returns an appropriate response based on the validation result.

        This method uses the `validate` method to validate the domain. If validation fails, it catches the exception,
        maps it to a corresponding error code, and returns a response based on the `return_type` parameter.

        Args:
            domain (str): The domain to validate.
            return_type (ValidationReturnType): Determines the type of response (JSON or form validation error).
            blank_ok (bool, optional): If True, blank input does not raise an exception. Defaults to False.

        Returns:
            tuple: The validated domain (or None if validation failed), and the response (success or error).
        """  # noqa

        # Map each exception to a corresponding error code
        error_map = {
            errors.BlankValueError: "required",
            errors.ExtraDotsError: "extra_dots",
            errors.DomainUnavailableError: "unavailable",
            errors.RegistrySystemError: "error",
            errors.InvalidDomainError: "invalid",
        }

        validated = None
        response = None

        try:
            # Attempt to validate the domain
            validated = cls.validate(domain, blank_ok)

        # Get a list of each possible exception, and the code to return
        except tuple(error_map.keys()) as error:
            # If an error is caught, get its type
            error_type = type(error)

            # Generate the response based on the error code and return type
            response = DomainHelper._return_form_error_or_json_response(return_type, code=error_map.get(error_type))
        else:
            # For form validation, we do not need to display the success message
            if return_type != ValidationReturnType.FORM_VALIDATION_ERROR:
                response = DomainHelper._return_form_error_or_json_response(return_type, code="success", available=True)

        # Return the validated domain and the response (either error or success)
        return (validated, response)

    @staticmethod
    def _return_form_error_or_json_response(return_type: ValidationReturnType, code, available=False):
        """
        Returns an error response based on the `return_type`.

        If `return_type` is `FORM_VALIDATION_ERROR`, raises a form validation error.
        If `return_type` is `JSON_RESPONSE`, returns a JSON response with 'available', 'code', and 'message' fields.
        If `return_type` is neither, raises a ValueError.

        Args:
            return_type (ValidationReturnType): The type of error response.
            code (str): The error code for the error message.
            available (bool, optional): Availability, only used for JSON responses. Defaults to False.

        Returns:
            A JSON response or a form validation error.

        Raises:
            ValueError: If `return_type` is neither `FORM_VALIDATION_ERROR` nor `JSON_RESPONSE`.
        """  # noqa
        match return_type:
            case ValidationReturnType.FORM_VALIDATION_ERROR:
                raise forms.ValidationError(DOMAIN_API_MESSAGES[code], code=code)
            case ValidationReturnType.JSON_RESPONSE:
                return JsonResponse({"available": available, "code": code, "message": DOMAIN_API_MESSAGES[code]})
            case _:
                raise ValueError("Invalid return type specified")

    @classmethod
    def sld(cls, domain: str):
        """
        Get the second level domain. Example: `gsa.gov` -> `gsa`.

        If no TLD is present, returns the original string.
        """
        return domain.split(".")[0]

    @classmethod
    def tld(cls, domain: str):
        """Get the top level domain. Example: `gsa.gov` -> `gov`."""
        parts = domain.rsplit(".")
        return parts[-1] if len(parts) > 1 else ""

src/registrar/models/utility/domain_helper.py
import re

from django import forms
from django.http import JsonResponse

from api.views import DOMAIN_API_MESSAGES, check_domain_available
from registrar.utility import errors
from epplibwrapper.errors import RegistryError
from registrar.utility.enums import ValidationReturnType


class DomainHelper:
    """Utility functions and constants for domain names."""

    # a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
    # begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
    DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")

    # a domain can be no longer than 253 characters in total
    MAX_LENGTH = 253

    @classmethod
    def string_could_be_domain(cls, domain: str | None) -> bool:
        """Return True if the string could be a domain name, otherwise False."""
        if not isinstance(domain, str):
            return False
        return bool(cls.DOMAIN_REGEX.match(domain))

    @classmethod
    def validate(cls, domain: str, blank_ok=False) -> str:
        """Attempt to determine if a domain name could be requested."""

        # Split into pieces for the linter
        domain = cls._validate_domain_string(domain, blank_ok)

        try:
            if not check_domain_available(domain):
                raise errors.DomainUnavailableError()
        except RegistryError as err:
            raise errors.RegistrySystemError() from err
        return domain

    @staticmethod
    def _validate_domain_string(domain, blank_ok):
        """Normalize the domain string, and check its content"""
        if domain is None:
            raise errors.BlankValueError()

        if not isinstance(domain, str):
            raise errors.InvalidDomainError()

        domain = domain.lower().strip()

        if domain == "" and not blank_ok:
            raise errors.BlankValueError()
        elif domain == "":
            # If blank ok is true, just return the domain
            return domain

        if domain.startswith("www."):
            domain = domain[4:]

        if domain.endswith(".gov"):
            domain = domain[:-4]

        if "." in domain:
            raise errors.ExtraDotsError()

        if not DomainHelper.string_could_be_domain(domain + ".gov"):
            raise errors.InvalidDomainError()

        return domain

    @classmethod
    def validate_and_handle_errors(cls, domain, return_type, blank_ok=False):
        """
        Validates a domain and returns an appropriate response based on the validation result.

        This method uses the `validate` method to validate the domain. If validation fails, it catches the exception,
        maps it to a corresponding error code, and returns a response based on the `return_type` parameter.

        Args:
            domain (str): The domain to validate.
            return_type (ValidationReturnType): Determines the type of response (JSON or form validation error).
            blank_ok (bool, optional): If True, blank input does not raise an exception. Defaults to False.

        Returns:
            tuple: The validated domain (or None if validation failed), and the response (success or error).
        """  # noqa

        # Map each exception to a corresponding error code
        error_map = {
            errors.BlankValueError: "required",
            errors.ExtraDotsError: "extra_dots",
            errors.DomainUnavailableError: "unavailable",
            errors.RegistrySystemError: "error",
            errors.InvalidDomainError: "invalid",
        }

        validated = None
        response = None

        try:
            # Attempt to validate the domain
            validated = cls.validate(domain, blank_ok)

        # Get a list of each possible exception, and the code to return
        except tuple(error_map.keys()) as error:
            # If an error is caught, get its type
            error_type = type(error)

            # Generate the response based on the error code and return type
            response = DomainHelper._return_form_error_or_json_response(return_type, code=error_map.get(error_type))
        else:
            # For form validation, we do not need to display the success message
            if return_type != ValidationReturnType.FORM_VALIDATION_ERROR:
                response = DomainHelper._return_form_error_or_json_response(return_type, code="success", available=True)

        # Return the validated domain and the response (either error or success)
        return (validated, response)

    @staticmethod
    def _return_form_error_or_json_response(return_type: ValidationReturnType, code, available=False):
        """
        Returns an error response based on the `return_type`.

        If `return_type` is `FORM_VALIDATION_ERROR`, raises a form validation error.
        If `return_type` is `JSON_RESPONSE`, returns a JSON response with 'available', 'code', and 'message' fields.
        If `return_type` is neither, raises a ValueError.

        Args:
            return_type (ValidationReturnType): The type of error response.
            code (str): The error code for the error message.
            available (bool, optional): Availability, only used for JSON responses. Defaults to False.

        Returns:
            A JSON response or a form validation error.

        Raises:
            ValueError: If `return_type` is neither `FORM_VALIDATION_ERROR` nor `JSON_RESPONSE`.
        """  # noqa
        match return_type:
            case ValidationReturnType.FORM_VALIDATION_ERROR:
                raise forms.ValidationError(DOMAIN_API_MESSAGES[code], code=code)
            case ValidationReturnType.JSON_RESPONSE:
                return JsonResponse({"available": available, "code": code, "message": DOMAIN_API_MESSAGES[code]})
            case _:
                raise ValueError("Invalid return type specified")

    @classmethod
    def sld(cls, domain: str):
        """
        Get the second level domain. Example: `gsa.gov` -> `gsa`.

        If no TLD is present, returns the original string.
        """
        return domain.split(".")[0]

    @classmethod
    def tld(cls, domain: str):
        """Get the top level domain. Example: `gsa.gov` -> `gov`."""
        parts = domain.rsplit(".")
        return parts[-1] if len(parts) > 1 else ""

src/registrar/models/utility/domain_helper.py
diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py
index a808ef803..3267f0c93 100644
--- a/src/registrar/models/utility/domain_helper.py
+++ b/src/registrar/models/utility/domain_helper.py
@@ -57,6 +57,9 @@ def _validate_domain_string(domain, blank_ok):
             # If blank ok is true, just return the domain
             return domain
 
+        if domain.startswith("www."):
+            domain = domain[4:]
+
         if domain.endswith(".gov"):
             domain = domain[:-4]
 
diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py
index a8d84ba7b..c9dd3c4f0 100644
--- a/src/registrar/tests/test_forms.py
+++ b/src/registrar/tests/test_forms.py
@@ -64,6 +64,12 @@ def test_requested_domain_valid(self):
         form = DotGovDomainForm(data={"requested_domain": "top-level-agency"})
         self.assertEqual(len(form.errors), 0)
 
+    def test_requested_domain_starting_www(self):
+        """Test a valid domain name with .www at the beginning."""
+        form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"})
+        self.assertEqual(len(form.errors), 0)
+        self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
+
     def test_requested_domain_ending_dotgov(self):
         """Just a valid domain name with .gov at the end."""
         form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"})

Handle www. prefix when input in /availability API
[PR#100 in getgov-home for additional context
](https://github.com/cisagov/getgov-home/pull/100)
Handle edge case of including [www](http://www/). in the search input. This is most likely best handled by updating the manage.get.gov's availability endpoint to handle the [www](http://www/). prefix when parsing arguments, similarly to how the API handles the .gov suffix.

Per Katherine:
 I envisioned that it would strip out the "www." when checking availability. So the confirmation message for "[www.example.gov](http://www.example.gov/)" would read: "[example.gov](http://example.gov/) is not available" Is that what you were thinking,

Example: 
 if [example.gov](http://example.gov/) was not available to begin with. I would think yes it strips www. then if [example.gov](http://example.gov/) is already taken it says “[example.gov](http://example.gov/) is not available”. If [example.gov](http://example.gov/) is actually available then entering [www.example.gov](http://www.example.gov/) would result in “[example.gov](http://example.gov/) is available”. Basically have it just ignore a www. at the start.

_Originally posted by @erinysong in https://github.com/cisagov/manage.get.gov/issues/476#issuecomment-1802870748_
            
[Slack thread](https://cisa-corp.slack.com/archives/C05BDEA3C11/p1705599697584059)
