from datetime import datetime
from flask import request, session, redirect
from flask_restful import Resource
from flask_login import login_user, logout_user, current_user
import re
import uuid
import os
import requests

from .anyaccessmodel import User, UserList, UserType, UserTypeList, Logs, LogsList
from .anymodel import Representation as RP, db, bcrypt, login_manager, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REDIRECT_URI, GOOGLE_RETURN_FRONTEND

from sqlalchemy.exc import OperationalError, ProgrammingError

import importlib.util

def load_peer_init():
  peer_init_path = os.path.join(os.getcwd(), 'init.py')
  
  if os.path.exists(peer_init_path):
    try:
      spec = importlib.util.spec_from_file_location("peer_init", peer_init_path)
      peer_module = importlib.util.module_from_spec(spec)
      spec.loader.exec_module(peer_module)
      
      if hasattr(peer_module, 'anyaccess_initcall'):
        print("ANYFLASK LOG: Local anyaccess_initcall found via absolute path.")
        return peer_module.anyaccess_initcall
    except Exception as e:
      print(f"ANYFLASK ERR: Failed to load local init: {e}")
  return None

anyaccess_initcall = load_peer_init()

if anyaccess_initcall is None:
  from .anymodel import anyaccess_initcall
  print("ANYFLASK WRN: Using internal anyaccess_initcall fallback.")

@login_manager.user_loader
def load_user(user_id):
  return User.query.get(int(user_id))

class Account(Resource):
  def post(self):
    register_data = RP.Arguments([ 'required:username', 'required:password', 'name', 'required:email', 'mobilenumber', 'usertype'])
    args = register_data.parse_args()
    
    if args.username == 'self' or args.username.startswith('google-'):
      return RP.Message('422:Protected username used')

    try:
      user = User.query.first()
    except (OperationalError, ProgrammingError):
      db.create_all()
      user = None

    if user is not None:
      message,status,username_current,usertype_current = Authenticate.authenticitycheck(current_user,"#modaccount,")
      if status != 200:
        return RP.Message(str(status) + ':' + message)

      user_check = User.query.filter_by(username=args.username).first()
      if user_check:
        return RP.Message('422:' + user_check.username + 'already exist')
      
      usertype_new = UserType.query.filter_by(usertype=args.usertype).first()
      if usertype_new is None:
        return RP.Message('427:Unrecognizable usertype')
      if args.usertype == 'superuser':
        return RP.Message('403:Forbidden to create superuser')
      if '#modadmin,' in usertype_new.tags:
        if '#modadmin,' not in usertype_current.tags:
          return RP.Message('403:Current user has no ability to create an admin account')

      upchar, lowchar, numchar = True, True, True
      if len(args.password) < 8:
        return RP.Message('422:Requires at least 8 character password')
      for passcheck in args.password:
        if (passcheck.islower()): lowchar=False
        if (passcheck.isupper()): upchar=False
        if (passcheck.isdigit()): numchar=False
      if (upchar) or (lowchar) or (numchar):
        return RP.Message('422:Password must contain at least a lower character, an upper character, and a number')
      
      regex = r'^[\w._-]+@[\w._-]+\.\w{2,3}$'
      if not(re.search(regex, args.email)):
        return RP.Message('422:Invalid Email')
        
    else:
      if args.usertype != 'superuser':
        return RP.Message('427:First user should be a superuser')
      else:
        username_current = args.username
        AccountType.initializetype()
        anyaccess_initcall()

    hashed_password = bcrypt.generate_password_hash(args.password).decode('utf-8')
    user = User(userid=str(uuid.uuid4()), username=args.username, password=hashed_password, usertype=args.usertype, name=args.name, mobilenumber=args.mobilenumber, email=args.email)
    db.session.add(user)
    db.session.commit()
    
    Journal.add("create account", args.username + " created", username_current)
    return RP.Message('201:' + user.username)

  def put(self):
    modify_data = RP.Arguments([ 'required:username', 'new_password', 'confirm_password', 'name', 'email', 'status', 'mobilenumber', 'usertype'])
    args = modify_data.parse_args()

    message,status,username_current,usertype_current = Authenticate.authenticitycheck(current_user,"#modaccount,")
    if status != 200:
      return RP.Message(str(status) + ':' + message)
    
    if args.username == 'self':
      return RP.Message('422:Protected username used')
    
    user_check = User.query.filter_by(username=args.username).first()
    if user_check is None:
      return RP.Message('427:User does not exist')

    if args.usertype is None or args.usertype == "":
      args.usertype = user_check.usertype

    usertype_new = UserType.query.filter_by(usertype=args.usertype).first()
    if usertype_new is None:
      return RP.Message('400:Unrecognizable usertype')
    if args.usertype == 'superuser' and current_user.usertype != 'superuser':
      return RP.Message('403:Forbidden to edit superuser')
    if '#modadmin,' not in usertype_current.tags:
      if '#modadmin,' in usertype_new.tags:
        return RP.Message('403:Current user has no ability to edit an admin account')
      usertype_old = UserType.query.filter_by(usertype=user_check.usertype).first()
      if usertype_old and '#modadmin,' in usertype_old.tags:
        return RP.Message('403:Current user has no ability to edit an admin account')
    user_check.usertype = args.usertype
    
    upchar, lowchar, numchar = True, True, True
    if args.new_password is not None and args.new_password != "":
      if args.new_password != args.confirm_password:
        return RP.Message('400:Confirmed password not equal to the new password')
      if len(args.new_password) < 8:
        return RP.Message('422:Requires at least 8 character password')
      for passcheck in args.new_password:
        if (passcheck.islower()): lowchar=False
        if (passcheck.isupper()): upchar=False
        if (passcheck.isdigit()): numchar=False
      if (upchar) or (lowchar) or (numchar):
        return RP.Message('422:Password must contain at least a lower character, an upper character, and a number')
      new_hashed_password = bcrypt.generate_password_hash(args.new_password).decode('utf-8')
      user_check.password = new_hashed_password

    if args.email is not None and args.email != "":
      regex = r'^[\w._-]+@[\w._-]+\.\w{2,3}$'
      if not(re.search(regex, args.email)):
        return RP.Message('422:Invalid Email')
      user_check.email = args.email
      if args.username.startswith('google-'):
        return RP.Message('427:Protected email address')

    if args.mobilenumber is not None and args.mobilenumber != "":
      user_check.mobilenumber = args.mobilenumber

    if args.name is not None and args.name != "":
      user_check.name = args.name
      if args.username.startswith('google-'):
        return RP.Message('427:Protected name')
      
    old_status,new_status,status=RP.status_check(user_check,args)
    if status: return RP.Message('422:Invalid Status')

    if old_status != new_status and args.username == current_user.username:
      return RP.Message('427:Unable to change own status')

    db.session.commit()

    if old_status == 1 and new_status == 0: Journal.add("edit account", args.username + " activated", username_current)
    elif old_status == 0 and new_status == 1: Journal.add("edit account", args.username + " archieved", username_current)
    else: Journal.add("edit account", args.username + " edited", username_current)    
    return RP.Message('201:' + args.username)

  def get(self):
    list_data = RP.ListArguments()
    args = list_data.parse_args()
    args.status = RP.status_act(args.status)

    if current_user.is_authenticated:
      Authenticate.lastactivity(current_user.username)
      if args.search == 'self':
        args.search = current_user.username
      usertype_current = UserType.query.filter_by(usertype=current_user.usertype).first()
      if usertype_current is None:
        return RP.Message('427:Current user has unknown usertype')
      if "#viewaccount," not in usertype_current.tags:
        if args.search == current_user.username:
          user_search = User.query.filter_by(username=args.search).first()
          if user_search is None:
            return RP.Message('427:No user ' + args.search + ' found')
          return RP.MessPage(User.jsonify(user_search),'success')
        return RP.Message('403:Current user has no ability to view an account')
    else:
      return RP.Message('401:Unauthorized')

    if args.search is not None and args.search != "":
      user_search = User.query.filter_by(username=args.search).first()
      if user_search is None:
        return RP.Message('427:No user found')
      return RP.MessPage(User.jsonify(user_search),'success')

    desc = RP.is_descending(args.sort_order) if args.sort_order is not None else True
    table = User.query

    if args.status is not None:
      table = table.filter_by(status=args.status)

    if args.filter_value is not None and args.filter_type is not None:
      if args.filter_type != "" and args.filter_value != "":
        if args.filter_type not in UserList:
          return RP.Message('422:Invalid filter type used')
        filterer = '%' + args.filter_value + '%'
        table = table.filter(getattr(User,args.filter_type).like(filterer))

    if args.order_by is not None and args.order_by != "":
      if args.order_by not in UserList:
        return RP.Message('422:Invalid order by used')
      table = table.order_by(getattr(User,args.order_by).desc()) if desc else table.order_by(getattr(User,args.order_by))
    else:
      table = table.order_by(User.id.desc()) if desc else table.order_by(User.id)

    if args.per_page is not None and args.per_page != "":
      current_page = args.current_page if args.current_page is not None else 1
      table = table.paginate(per_page=int(args.per_page),page=int(current_page))
      table_list = User.jsonify_list(table.items)
      return RP.MessPage(table_list,RP.Page_Details(table))
    else:
      table_list = User.jsonify_list(table.all())
      return RP.MessPage(table_list,'success')

class AccountType(Resource):
  @staticmethod
  def initializetype():
    usertype = UserType.query.first()
    if usertype is None:
      types = [
        ("superuser", "#modadmin,#modaccount,#viewaccount,#modusertype,#viewusertype,#viewlog,#none"),
        ("mainadmin", "#modadmin,#modaccount,#viewaccount,#modusertype,#viewusertype,#viewlog,#none"),
        ("admin", "#modaccount,#viewaccount,#viewusertype,#viewlog,#none"),
        ("user", "#none,")
      ]
      for utype, tags in types:
        db.session.add(UserType(usertype=utype, status=0, tags=tags))
      db.session.commit()

  def post(self):
    register_data = RP.Arguments([ 'required:usertype', 'required:tags'])
    args = register_data.parse_args()
    
    message,status,username_current,usertype_current = Authenticate.authenticitycheck(current_user,"#modusertype,")
    if status != 200:
      return RP.Message(str(status) + ':' + message)
    
    user_check = UserType.query.filter_by(usertype=args.usertype).first()
    if user_check is not None:
      return RP.Message('422:Usertype already exist')

    if args.tags is not None and args.tags != "":
      args.tags=args.tags.replace(" ","")
      if args.tags[-1:] != ',':
        args.tags = args.tags + ','
      tagsplit = args.tags.split(',')
      for tag in tagsplit[:-1]:
        if tag[0] != '#':
          return RP.Message('427:Tags should start with hashtag')
        if tag + ',' not in str(usertype_current.tags) + ",":
          if usertype_current.usertype != 'superuser':
            return RP.Message('403:Current user has no ability to add this tag' + tag)
    else:
      args.tags = "#none,"
    
    usertype = UserType(usertype=args.usertype, tags=args.tags)
    db.session.add(usertype)
    db.session.commit()

    Journal.add("create usertype", args.usertype + " created", username_current)
    return RP.Message('201:' + args.usertype)

  def put(self):
    modify_data = RP.Arguments([ 'usertype', 'tags', 'status'])
    args = modify_data.parse_args()
    
    message,status,username_current,usertype_current = Authenticate.authenticitycheck(current_user,"#modusertype,")
    if status != 200:
      return RP.Message(str(status) + ':' + message)
      
    user_check = UserType.query.filter_by(usertype=args.usertype).first()
    if user_check is None:
      return RP.Message('427:Usertype does not exist')

    if args.tags is not None:
      args.tags=args.tags.replace(" ","")
      if args.tags[-1:] != ',':
        args.tags = args.tags + ','
      tagsplit = args.tags.split(',')
      for tag in tagsplit[:-1]:
        if tag[0] != '#':
          return RP.Message('427:Tags should start with hashtag')
        if tag + ',' not in str(usertype_current.tags) + ",":
          if usertype_current.usertype != 'superuser':
            return RP.Message('403:Current user has no ability to add this tag' + tag)
      user_check.tags = args.tags
    
    old_status,new_status,status=RP.status_check(user_check,args)
    if status: return RP.Message('422:Invalid Status')
      
    db.session.commit()

    if old_status == 1 and new_status == 0: Journal.add("edit account", args.usertype + " activated", username_current)
    elif old_status == 0 and new_status == 1: Journal.add("edit account", args.usertype + " archieved", username_current)
    else: Journal.add("edit account", args.usertype + " edited", username_current)
    return RP.Message('201:' + args.usertype)
      
  def get(self):
    list_data = RP.ListArguments()
    args = list_data.parse_args()
    args.status = RP.status_act(args.status)

    message,status,_,_ = Authenticate.authenticitycheck(current_user,"#viewusertype,")
    if status != 200:
      return RP.Message(str(status) + ':' + message)

    if args.search is not None and args.search != "":
      user_search = UserType.query.filter_by(usertype=args.search).first()
      if user_search is None:
        return RP.Message('427:no usertype found')
      return RP.MessPage(UserType.jsonify(user_search),'success')

    desc = RP.is_descending(args.sort_order) if args.sort_order is not None else True
    table = UserType.query

    if args.status is not None:
      table = table.filter_by(status=args.status)

    if args.filter_value is not None and args.filter_type is not None:
      if args.filter_type != "" and args.filter_value != "":
        if args.filter_type not in UserTypeList:
          return RP.Message('422:Invalid filter type used')
        filterer = '%' + args.filter_value + '%'
        table = table.filter(getattr(UserType,args.filter_type).like(filterer))

    if args.order_by is not None and args.order_by != "":
      if args.order_by not in UserTypeList:
        return RP.Message('422:Invalid order by used')
      table = table.order_by(getattr(UserType,args.order_by).desc()) if desc else table.order_by(getattr(UserType,args.order_by))
    else:
      table = table.order_by(UserType.id.desc()) if desc else table.order_by(UserType.id)

    if args.per_page is not None and args.per_page != "":
      current_page = args.current_page if args.current_page is not None else 1
      table = table.paginate(per_page=int(args.per_page),page=int(current_page))
      table_list = UserType.jsonify_list(table.items)
      return RP.MessPage(table_list,RP.Page_Details(table))
    else:
      table_list = UserType.jsonify_list(table.all())
      return RP.MessPage(table_list,'success')
      
class Journal(Resource):
  def get(self):
    list_data = RP.ListArguments()
    args = list_data.parse_args()
    
    message,status,_,_ = Authenticate.authenticitycheck(current_user,"#viewlog,")
    if status != 200:
      return RP.Message(str(status) + ':' + message)

    desc = RP.is_descending(args.sort_order) if args.sort_order is not None else True
    table = Logs.query

    if args.filter_value is not None and args.filter_type is not None:
      if args.filter_type != "" and args.filter_value != "":
        if args.filter_type not in LogsList:
          return RP.Message('422:Invalid filter type used')
        filterer = '%' + args.filter_value + '%'
        table = table.filter(getattr(Logs,args.filter_type).like(filterer))

    if args.order_by is not None and args.order_by != "":
      if args.order_by not in LogsList:
        return RP.Message('422:Invalid order by used')
      table = table.order_by(getattr(Logs,args.order_by).desc()) if desc else table.order_by(getattr(Logs,args.order_by))
    else:
      table = table.order_by(Logs.id.desc()) if desc else table.order_by(Logs.id)

    if args.per_page is not None and args.per_page != "":
      current_page = args.current_page if args.current_page is not None else 1
      table = table.paginate(per_page=int(args.per_page),page=int(current_page))
      table_list = Logs.jsonify_list(table.items)
      return RP.MessPage(table_list,RP.Page_Details(table))
    else:
      table_list = Logs.jsonify_list(table.all())
      return RP.MessPage(table_list,'success')

  @staticmethod
  def add(logtype,log,action_by):
    logs = Logs(logtype = logtype, log = log, action_by = action_by)
    db.session.add(logs)
    db.session.commit()

  @staticmethod
  def addservice(service, logtype, log, action_by):
    logs = Logs(service=service, logtype = logtype, log = log, action_by = action_by)
    db.session.add(logs)
    db.session.commit()

class Authenticate(Resource):
  def post(self):
    parser = RP.Arguments([ 'required:username', 'required:password'])
    args = parser.parse_args()
    user = User.query.filter_by(username=args.username).first()

    if args.username == 'self' or args.username.startswith('google-'):
      return RP.Message('422:Protected username used')

    if user and bcrypt.check_password_hash(user.password,args.password):
      usertype_current = UserType.query.filter_by(usertype=user.usertype).first()
      if not usertype_current or usertype_current.status != 0:
        return RP.Message('422:user has inactive usertype')
      if user.status != 0:
        return RP.Message('403:forbidden')
      Authenticate.lastactivity(args.username)
      login_user(user)
      Journal.add("authentication","signed in on " + request.remote_addr, args.username)
      return RP.Message('201:authenticated')
    else:
      return RP.Message('401:wrong username or password')

  def put(self):
    if current_user.is_authenticated:
      usertype_current = UserType.query.filter_by(usertype=current_user.usertype).first()
      if not usertype_current or usertype_current.status != 0:
        logout_user()
        session.clear()
        return RP.Message('403:forbidden')
      Authenticate.lastactivity(current_user.username)
      Journal.add("authentication", "opened the app on " + request.remote_addr, current_user.username)
      return RP.Message('200:dashboard')
    return RP.Message('200:login')
    
  def get(self):
    if current_user.is_authenticated:
      Authenticate.lastactivity(current_user.username)
      logs = Logs(logtype="authentication", log = "signed out", action_by = current_user.username)
      db.session.add(logs)
      db.session.commit()
      logout_user()
      session.clear()
      return RP.Message('201:signed off')
    return RP.Message('201:already signed off')
  
  @staticmethod
  def lastactivity(username):
    user = User.query.filter_by(username=username).first()
    if user is not None:
      user.last_activity = datetime.now()
      db.session.commit()
    return None

  @staticmethod
  def authenticitycheck(current_user,required_tag):
    if current_user.is_authenticated:
      Authenticate.lastactivity(current_user.username)
      username_current = current_user.username
      usertype_current = UserType.query.filter_by(usertype=current_user.usertype).first()
      if usertype_current is None:
        return 'Unauthorized', 401, '', ''
      if required_tag not in usertype_current.tags:
        return 'Insufficient Permissions', 403, '', ''
    else:
      return 'Unauthorized', 401, '', ''
    return '', 200, username_current, usertype_current

class GoogleAuth(Resource):
  def get(self):
    list_data = RP.GoogleArguments()
    args = list_data.parse_args()

    token_url = "https://oauth2.googleapis.com/token"
    data = {
      "code": str(args.code),
      "client_id": GOOGLE_CLIENT_ID,
      "client_secret": GOOGLE_CLIENT_SECRET,
      "redirect_uri": GOOGLE_REDIRECT_URI,
      "grant_type": "authorization_code",
    }

    token_res = requests.post(token_url, data=data)
    token_json = token_res.json()
    id_token = token_json.get("id_token")

    user_info_res = requests.get(f"https://oauth2.googleapis.com/tokeninfo?id_token={id_token}")
    user_info = user_info_res.json()

    # print(f"DEBUG USER_INFO: {user_info}")
    username = 'google-' + str(user_info.get("sub"))

    user_check = User.query.filter_by(username=username).first()
    if user_check:
      usertype_current = UserType.query.filter_by(usertype=user_check.usertype).first()
      if not usertype_current or usertype_current.status != 0:
        return RP.Message('422:user has inactive usertype')
      if user_check.status != 0:
        return RP.Message('403:forbidden')
      Authenticate.lastactivity(username)
      login_user(user_check)
      Journal.add("authentication","signed in on " + request.remote_addr, username)
    else:
      try:
        User.query.first()
      except (OperationalError, ProgrammingError):
        return RP.Message('427:Create a superuser first')
      Journal.add("create account", username + " created", 'google-OAUTH')
      user = User(userid=str(uuid.uuid4()), username=username, password="", usertype="user", name=user_info.get("name"), mobilenumber="", email=user_info.get("email"))
      db.session.add(user)
      db.session.commit()

    return redirect(GOOGLE_RETURN_FRONTEND)
