import concurrent
from asgiref.sync import sync_to_async
import time
import asyncio
from django.contrib.auth.decorators import login_required
from ninja.security import django_auth, HttpBearer
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from ninja.responses import codes_4xx
from .models import Task, Employee, Department, Client, api_key
from typing import List
from ninja import Router, Path, Query, Form, File
from ninja.files import UploadedFile
from .schemas import (
    PathDate,
    Filters,
    Item,
    UserIn,
    TaskIn,
    UserOut,
    TaskSchema,
    EmployeeIn,
    EmployeeOut,
    DepartmentOut,
    DepartmentIn,
)

# from elasticsearch import AsyncElasticsearch
from django.contrib.auth import get_user_model

User = get_user_model()


# es = AsyncElasticsearch()
router = Router()


# path parameter with types
@router.get("/items/{item_id}")
def read_item(request, item_id: int):
    return {"item_id": item_id}


# multiple parameters


@router.get("/events/{year}/{month}/{date}")
def events(request, date: PathDate = Path(...)):
    return {"date": date.value()}


@router.get("/filter")
def events(request, filters: Filters = Query(...)):
    return {"filters": filters.dict()}


# query parameters
"""
When you declare other function parameters that are not part of the path parameters,
they are automatically interpreted as "query" parameters.
"""
weapons = [
    "Ninjaro",
    "Shuriken",
    "Katana",
    "Kama",
    "Kunai",
    "Kunai",
    "Naginata",
    "Yari",
]


@router.get("/weapons")
def list_weapons(request, limit: int = 10, offset: int = 0):
    return weapons[offset : offset + limit]


# required & optional parameters


@router.get("/weapons/search")
def search_weapons(request, q: str, offset: int = 0):
    results = [w for w in weapons if q.lower() in w.lower()]
    # print(q, results)
    return results[offset : offset + 10]


# request body(used with 'create' and 'update')
"""
To declare a request body you need to use Django Ninja Schema
"""


@router.post("/items")
def create(request, item: Item):
    return item


# request body + path parameters
"""
Django Ninja will recognize that the function parameters that match path parameters
 should be taken from the path, and that function parameters that are declared with
  Schema should be taken from the request body.
"""


@router.put("/items/{item_id}")
def update(request, item_id: int, item: Item):
    return {"item_id": item_id, "item": item.dict()}


# Request body + Path + Query parameers
@router.post("/items/{item_id}")
def update(request, item_id: int, item: Item, q: str):
    return {"item_id": item_id, "item": item.dict(), "q": q}


# from data
@router.post("/login")
def login(request, username: str = Form(...), password: str = Form(...)):
    return {"username": username, "password": password}


# file uploads


@router.post("/upload")
def upload(request, file: UploadedFile = File(...)):
    data = file.read()
    return {"name": file.name, "len": len(data)}


@router.post("/upload_many")
def upload_many(request, files: List[UploadedFile] = File(...)):
    return [f.name for f in files]


@router.post("/users/", response=UserOut)
def create_user(request, data: UserIn):
    user = User(username=data.username)  # User in django auth.user
    user.set_password(<PASSWORD>)
    user.save()
    return user


"""
@router.get('/tasks', response=List[TaskSchema])
def tasks(request):
    quryset = Task.objects.all()
    return list(quryset)
"""
# alternative


@router.get("/tasks", response=List[TaskSchema])
def tasks(request):
    return Task.objects.all()


@router.post("/task")
def create_task(request, payload: TaskIn):
    data = payload.dict()
    try:
        # if User.objects.filter(id=data['owner']).exists():
        owner = User.objects.get(id=data["owner"])
        del data["owner"]
        new_task = Task.objects.create(owner=owner, **data)
        return {
            "detail": "Task created successfully!",
            "id": new_task.id,
            "title": new_task.title,
        }
    except User.DoesNotExist:
        return {"detail": "The specific user does not exists!"}


# response schema

# def login(request, payload: Auth):
#     if auth_not_valid:
#         return 401, {'message': 'Unauthorized'}
#     if negative_balance:
#         return 402, {'message': 'Insufficient balance amount Please proceed to a payment page' }
#     return 200, {'token': xxx}

# CRUD
@router.post("/employees", tags=["employees"])
def create_employee(request, payload: EmployeeIn):
    try:
        department = Department.objects.get(id=payload.dict()["department_id"])
        employee = Employee.objects.create(**payload.dict())
        return {
            "detail": _(
                f"Employee '{payload.dict()['last_name']}' created successfully."
            ),
            "id": employee.id,
        }
    except Department.DoesNotExist:
        return {"detail": _(f"Depertment does not exists!")}


@router.get("/employees/{employee_id}", response=EmployeeOut, tags=["employees"])
def get_employee(request, employee_id: int):
    employee = get_object_or_404(Employee, id=employee_id)
    return employee


# @router.get('/employees', response=List[EmployeeOut])
# def list_employees(request):
#     return Employee.objects.all()


@router.get("/employees", response=List[EmployeeOut], tags=["employees"])
def list_employees(request):
    start = time.time()
    employees = []
    for emp in Employee.objects.all():
        employees.append(emp)
    print("time", time.time() - start)
    return employees


@router.get("/employees1", response=List[EmployeeOut], tags=["employees"])
def _asnyc_list_employees(request):
    return Employee.objects.filter()


async_list_employees = sync_to_async(_asnyc_list_employees, thread_sensitive=True)


# @router.get('/employees2', response=List[EmployeeOut], tags=['employees'])
# def conc_list_employees(request):
#     start = time.time()
#     employees = []
#     with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
#         for emp in executor.map(Employees.objects.all())
#         employees.append(emp)
#     print(time.time()-start)
#     return employees


@router.post("/departments")
def create_department(request, payload: DepartmentIn):
    data = payload.dict()
    if Department.objects.filter(title=data["title"]).exists():
        return {"detail": _(f"Department '{data['title']}' already exists!")}
    # else:
    department = Department.objects.create(**payload.dict())
    return {"id": department.id}


@router.put("/employees/{employee_id}")
@transaction.atomic
def update_employee(request, employee_id: int, payload: EmployeeIn):
    try:
        with transaction.atomic():
            employee = Employee.objects.get(id=employee_id)
            department = Department.objects.get(id=payload.dict()["department_id"])
            for attr, value in payload.dict().items():
                setattr(employee, attr, value)
            employee.save()
            return {"success": True, "detail": _("Employee updated successfully")}
    except Department.DoesNotExist:
        return {"success": False, "detail": "Departent does not exist."}
    except Employee.DoesNotExist:
        return {"success": False, "detail": "Employee does not exist."}


@router.delete("/employee/{employee_id}")
@transaction.atomic
@login_required()
def delete_employee(request, employee_id: int):
    try:
        with transaction.atomic():
            employee = Employee.objects.get(id=employee_id)
            employee.delete()
            return {
                "success": True,
                "detail": _(f"Employee '{employee.last_name}' delete."),
            }
    except Employee.DoesNotExist:
        return {
            "success": False,
            "detail": _(f"Employee with id '{employee_id}' does not exists"),
        }


# authentication
@router.get("/pets", auth=django_auth)
def pets(request):
    return f"Authenticated user {request.auth}"


# Automatic OpenAPI schemma
# in order to authenticate, needs to pass a header
class AuthBearer(HttpBearer):
    def authenticate(self, request, token):
        if token == "supersecret":
            return token


@router.get("/bearer", auth=AuthBearer)
def bearer(request):
    return {"token", request.auth}


@router.post("/users")
def create_user(request, payload: UserIn):
    data = payload.dict()
    if User.objects.filter(username=data["username"]).exists():
        return {"detail": _(f"Username '{data['username']}' arleady taken.")}
    if User.objects.filter(email=data["email"]).exists():
        return {"detail": _(f"email '{data['email']}' arleady taken.")}
    else:
        user = User.objects.create_user(
            username=data["username"], email=data["email"], password=data["password"]
        )
        user.first_name = data["first_name"]
        user.last_name = data["last_name"]
        user.save()
        return {
            "status": 200,
            "detail": _(f"User '{data['username']}' created successfully."),
            "id": user.id,
        }


@router.post("/token", auth=None)  # overriding global auth
def get_token(request, username: str = Form(...), password: str = Form(...)):
    if username == "admin" and password == "<PASSWORD>":
        return {"token": "<PASSWORD>"}
    else:
        return {"detail": "Wrong credentials"}


@router.get(
    "/apikey",
    auth=api_key,
    tags=["core"],
    summary=_(f"authenticate apikey"),
    description=_(f"Authenticate user by using apikey"),
    deprecated=True,
)
def apikey(request, api_key):
    assert isinstance(request.auth, Client)
    return _(f"Hello {request.auth}")


# async
@router.get("/sync-employees", tags=["sync"], response=List[EmployeeOut])
def sync_emp(request):
    time.sleep(2)
    return Employee.objects.all()


@router.get("/async-employees", tags=["sync"], response=List[EmployeeOut])
async def async_emp(request):
    # this will throw an error when you iterate
    # employees = await sync_to_async(Employee.objects.all, thread_sensitive=True)()
    # use this instead
    employees = await sync_to_async(list)(Employee.objects.all())
    return employees


# @router.get('/search', tags=['sync'])
# async def search(request, q: str):
#     resp = await AsyncElasticsearch().search(
#         index='documents',
#         body={'query': {'query_string': {'query': q}}},
#         size=20,
#     )
#     return resp['hits']


@router.get("/search/{employee_id}", response=EmployeeOut, tags=["sync"])
async def search(request, employee_id: int):
    try:
        employee = await sync_to_async(Employee.objects.get)(id=employee_id)
        return employee
    except Employee.DoesNotExist:
        return {
            "status": 400,
            "detail": _(f"Employee with id{employee_id} does not exists"),
        }

import concurrent
from asgiref.sync import sync_to_async
import time
import asyncio
from django.contrib.auth.decorators import login_required
from ninja.security import django_auth, HttpBearer
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from ninja.responses import codes_4xx
from .models import Task, Employee, Department, Client, api_key
from typing import List
from ninja import Router, Path, Query, Form, File
from ninja.files import UploadedFile
from .schemas import (
    PathDate,
    Filters,
    Item,
    UserIn,
    TaskIn,
    UserOut,
    TaskSchema,
    EmployeeIn,
    EmployeeOut,
    DepartmentOut,
    DepartmentIn,
)

# from elasticsearch import AsyncElasticsearch
from django.contrib.auth import get_user_model

User = get_user_model()


# es = AsyncElasticsearch()
router = Router()


# path parameter with types
@router.get("/items/{item_id}")
def read_item(request, item_id: int):
    return {"item_id": item_id}


# multiple parameters


@router.get("/events/{year}/{month}/{date}")
def events(request, date: PathDate = Path(...)):
    return {"date": date.value()}


@router.get("/filter")
def events(request, filters: Filters = Query(...)):
    return {"filters": filters.dict()}


# query parameters
"""
When you declare other function parameters that are not part of the path parameters,
they are automatically interpreted as "query" parameters.
"""
weapons = [
    "Ninjaro",
    "Shuriken",
    "Katana",
    "Kama",
    "Kunai",
    "Kunai",
    "Naginata",
    "Yari",
]


@router.get("/weapons")
def list_weapons(request, limit: int = 10, offset: int = 0):
    return weapons[offset : offset + limit]


# required & optional parameters


@router.get("/weapons/search")
def search_weapons(request, q: str, offset: int = 0):
    results = [w for w in weapons if q.lower() in w.lower()]
    # print(q, results)
    return results[offset : offset + 10]


# request body(used with 'create' and 'update')
"""
To declare a request body you need to use Django Ninja Schema
"""


@router.post("/items")
def create(request, item: Item):
    return item


# request body + path parameters
"""
Django Ninja will recognize that the function parameters that match path parameters
 should be taken from the path, and that function parameters that are declared with
  Schema should be taken from the request body.
"""


@router.put("/items/{item_id}")
def update(request, item_id: int, item: Item):
    return {"item_id": item_id, "item": item.dict()}


# Request body + Path + Query parameers
@router.post("/items/{item_id}")
def update(request, item_id: int, item: Item, q: str):
    return {"item_id": item_id, "item": item.dict(), "q": q}


# from data
@router.post("/login")
def login(request, username: str = Form(...), password: str = Form(...)):
    return {"username": username, "password": password}


# file uploads


@router.post("/upload")
def upload(request, file: UploadedFile = File(...)):
    data = file.read()
    return {"name": file.name, "len": len(data)}


@router.post("/upload_many")
def upload_many(request, files: List[UploadedFile] = File(...)):
    return [f.name for f in files]


@router.post("/users/", response=UserOut)
def create_user(request, data: UserIn):
    user = User(username=data.username)  # User in django auth.user
    user.set_password(<PASSWORD>)
    user.save()
    return user


"""
@router.get('/tasks', response=List[TaskSchema])
def tasks(request):
    quryset = Task.objects.all()
    return list(quryset)
"""
# alternative


@router.get("/tasks", response=List[TaskSchema])
def tasks(request):
    return Task.objects.all()


@router.post("/task")
def create_task(request, payload: TaskIn):
    data = payload.dict()
    try:
        # if User.objects.filter(id=data['owner']).exists():
        owner = User.objects.get(id=data["owner"])
        del data["owner"]
        new_task = Task.objects.create(owner=owner, **data)
        return {
            "detail": "Task created successfully!",
            "id": new_task.id,
            "title": new_task.title,
        }
    except User.DoesNotExist:
        return {"detail": "The specific user does not exists!"}


# response schema

# def login(request, payload: Auth):
#     if auth_not_valid:
#         return 401, {'message': 'Unauthorized'}
#     if negative_balance:
#         return 402, {'message': 'Insufficient balance amount Please proceed to a payment page' }
#     return 200, {'token': xxx}

# CRUD
@router.post("/employees", tags=["employees"])
def create_employee(request, payload: EmployeeIn):
    try:
        department = Department.objects.get(id=payload.dict()["department_id"])
        employee = Employee.objects.create(**payload.dict())
        return {
            "detail": _(
                f"Employee '{payload.dict()['last_name']}' created successfully."
            ),
            "id": employee.id,
        }
    except Department.DoesNotExist:
        return {"detail": _(f"Depertment does not exists!")}


@router.get("/employees/{employee_id}", response=EmployeeOut, tags=["employees"])
def get_employee(request, employee_id: int):
    employee = get_object_or_404(Employee, id=employee_id)
    return employee


# @router.get('/employees', response=List[EmployeeOut])
# def list_employees(request):
#     return Employee.objects.all()


@router.get("/employees", response=List[EmployeeOut], tags=["employees"])
def list_employees(request):
    start = time.time()
    employees = []
    for emp in Employee.objects.all():
        employees.append(emp)
    print("time", time.time() - start)
    return employees


@router.get("/employees1", response=List[EmployeeOut], tags=["employees"])
def _asnyc_list_employees(request):
    return Employee.objects.filter()


async_list_employees = sync_to_async(_asnyc_list_employees, thread_sensitive=True)


# @router.get('/employees2', response=List[EmployeeOut], tags=['employees'])
# def conc_list_employees(request):
#     start = time.time()
#     employees = []
#     with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
#         for emp in executor.map(Employees.objects.all())
#         employees.append(emp)
#     print(time.time()-start)
#     return employees


@router.post("/departments")
def create_department(request, payload: DepartmentIn):
    data = payload.dict()
    if Department.objects.filter(title=data["title"]).exists():
        return {"detail": _(f"Department '{data['title']}' already exists!")}
    # else:
    department = Department.objects.create(**payload.dict())
    return {"id": department.id}


@router.put("/employees/{employee_id}")
@transaction.atomic
def update_employee(request, employee_id: int, payload: EmployeeIn):
    try:
        with transaction.atomic():
            employee = Employee.objects.get(id=employee_id)
            department = Department.objects.get(id=payload.dict()["department_id"])
            for attr, value in payload.dict().items():
                setattr(employee, attr, value)
            employee.save()
            return {"success": True, "detail": _("Employee updated successfully")}
    except Department.DoesNotExist:
        return {"success": False, "detail": "Departent does not exist."}
    except Employee.DoesNotExist:
        return {"success": False, "detail": "Employee does not exist."}


@router.delete("/employee/{employee_id}")
@transaction.atomic
@login_required()
def delete_employee(request, employee_id: int):
    try:
        with transaction.atomic():
            employee = Employee.objects.get(id=employee_id)
            employee.delete()
            return {
                "success": True,
                "detail": _(f"Employee '{employee.last_name}' delete."),
            }
    except Employee.DoesNotExist:
        return {
            "success": False,
            "detail": _(f"Employee with id '{employee_id}' does not exists"),
        }


# authentication
@router.get("/pets", auth=django_auth)
def pets(request):
    return f"Authenticated user {request.auth}"


# Automatic OpenAPI schemma
# in order to authenticate, needs to pass a header
class AuthBearer(HttpBearer):
    def authenticate(self, request, token):
        if token == "supersecret":
            return token


@router.get("/bearer", auth=AuthBearer)
def bearer(request):
    return {"token", request.auth}


@router.post("/users")
def create_user(request, payload: UserIn):
    data = payload.dict()
    if User.objects.filter(username=data["username"]).exists():
        return {"detail": _(f"Username '{data['username']}' arleady taken.")}
    if User.objects.filter(email=data["email"]).exists():
        return {"detail": _(f"email '{data['email']}' arleady taken.")}
    else:
        user = User.objects.create_user(
            username=data["username"], email=data["email"], password=data["password"]
        )
        user.first_name = data["first_name"]
        user.last_name = data["last_name"]
        user.save()
        return {
            "status": 200,
            "detail": _(f"User '{data['username']}' created successfully."),
            "id": user.id,
        }


@router.post("/token", auth=None)  # overriding global auth
def get_token(request, username: str = Form(...), password: str = Form(...)):
    if username == "admin" and password == "<PASSWORD>":
        return {"token": "<PASSWORD>"}
    else:
        return {"detail": "Wrong credentials"}


@router.get(
    "/apikey",
    auth=api_key,
    tags=["core"],
    summary=_(f"authenticate apikey"),
    description=_(f"Authenticate user by using apikey"),
    deprecated=True,
)
def apikey(request, api_key):
    assert isinstance(request.auth, Client)
    return _(f"Hello {request.auth}")


# async
@router.get("/sync-employees", tags=["sync"], response=List[EmployeeOut])
def sync_emp(request):
    time.sleep(2)
    return Employee.objects.all()


@router.get("/async-employees", tags=["sync"], response=List[EmployeeOut])
async def async_emp(request):
    # this will throw an error when you iterate
    # employees = await sync_to_async(Employee.objects.all, thread_sensitive=True)()
    # use this instead
    employees = await sync_to_async(list)(Employee.objects.all())
    return employees


# @router.get('/search', tags=['sync'])
# async def search(request, q: str):
#     resp = await AsyncElasticsearch().search(
#         index='documents',
#         body={'query': {'query_string': {'query': q}}},
#         size=20,
#     )
#     return resp['hits']


@router.get("/search/{employee_id}", response=EmployeeOut, tags=["sync"])
async def search(request, employee_id: int):
    try:
        employee = await sync_to_async(Employee.objects.get)(id=employee_id)
        return employee
    except Employee.DoesNotExist:
        return {
            "status": 400,
            "detail": _(f"Employee with id{employee_id} does not exists"),
        }

# from elasticsearch import AsyncElasticsearch # es = AsyncElasticsearch() # path parameter with types # multiple parameters # query parameters When you declare other function parameters that are not part of the path parameters, they are automatically interpreted as "query" parameters. # required & optional parameters # print(q, results) # request body(used with 'create' and 'update') To declare a request body you need to use Django Ninja Schema # request body + path parameters Django Ninja will recognize that the function parameters that match path parameters  should be taken from the path, and that function parameters that are declared with   Schema should be taken from the request body. # Request body + Path + Query parameers # from data # file uploads # User in django auth.user @router.get('/tasks', response=List[TaskSchema]) def tasks(request):     quryset = Task.objects.all()     return list(quryset) # alternative # if User.objects.filter(id=data['owner']).exists(): # response schema # def login(request, payload: Auth): #     if auth_not_valid: #         return 401, {'message': 'Unauthorized'} #     if negative_balance: #         return 402, {'message': 'Insufficient balance amount Please proceed to a payment page' } #     return 200, {'token': xxx} # CRUD # @router.get('/employees', response=List[EmployeeOut]) # def list_employees(request): #     return Employee.objects.all() # @router.get('/employees2', response=List[EmployeeOut], tags=['employees']) # def conc_list_employees(request): #     start = time.time() #     employees = [] #     with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor: #         for emp in executor.map(Employees.objects.all()) #         employees.append(emp) #     print(time.time()-start) #     return employees # else: # authentication # Automatic OpenAPI schemma # in order to authenticate, needs to pass a header # overriding global auth # async # this will throw an error when you iterate # employees = await sync_to_async(Employee.objects.all, thread_sensitive=True)() # use this instead # @router.get('/search', tags=['sync']) # async def search(request, q: str): #     resp = await AsyncElasticsearch().search( #         index='documents', #         body={'query': {'query_string': {'query': q}}}, #         size=20, #     ) #     return resp['hits']