<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Request;

use App\Account;
use App\Alias;
use App\Domain;
use App\DomainUser;
use App\TlsPolicy;
use App\User;
use App\DovecotPw;
use App\Entropy;

use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Exceptions\PermissionException;
use App\Exceptions\ErrorException;
use Illuminate\Validation\Rule;


class VMailController extends Controller
{
    const MinimumEntropy = 50;
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show the application dashboard.
     *
     * @return \Illuminate\Contracts\Support\Renderable
     */
    public function index()
    {
        if(Auth::user()->hasRole('admin'))
        {
            $domains = Domain::all();
            return view('layouts/vmail',['domains' => $domains]);
        }
        elseif(Auth::user()->hasRole('user'))
        {
            $domains = Auth::user()->cards;
            return view('layouts/vmail',['domains' => $domains]);
        }
        else
        {
            return view('layouts/unverified',);
        }


        return view('layouts/vmail',['domains' => $domains]);
    }

    public function domainsAjax(Request $request)
    {
        $action = $request->input('action');
        try
        {
            // block unverified users
            if(!(Auth::user()->hasRole('admin') || Auth::user()->hasRole('user'))){
                throw PermissionException('Not Authorized');
            }

            if($action == 'create')
            {
                if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}

                $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'unique:App\Domain,domain'],
                ]);
                $domain = new Domain();
                $domain->domain = $validatedData['domain'];
                $domain->save();
                $domain->users()->attach(Auth::user()->id,['role' => 'own']);

                return [$domain];
            }
            elseif($action == 'rename')
            {
                if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}

                $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'exists:App\Domain,domain'],
                    'newname' => ['required', 'string', 'unique:App\Domain,domain'],
                ]);

                $domain = Domain::where('domain',$validatedData['domain'])->first();

                $accounts = $domain->accounts;
                $aliases = $domain->aliases;
                $users = $domain->users;

                foreach($users as $u)
                {
                    $domain->users()->detach($u->id);
                }

                $domain->domain = $validatedData['newname'];
                $domain->save();

                foreach($accounts as $a)
                {
                    $a->domain()->associate($domain);
                    $a->save();
                }

                foreach($aliases as $a)
                {
                    $a->source_domain()->associate($domain);
                    $a->save();
                }

                foreach($users as $u)
                {
                    $domain->users()->attach($u->id);
                }
                $domain->save();

                return [$domain];
            }
            elseif($action == 'drop')
            {
                if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}

                $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'exists:App\Domain,domain'],
                ]);
                $domain = Domain::where('domain',$validatedData['domain'])->first();

                // remove all aliases and accounts related to this domain
                $domain->delete();
                foreach(Account::where('domain',$validatedData['domain'])->get() as $a){
                    $a->delete();
                }
                foreach(Alias::where('source_domain',$validatedData['domain'])->get() as $a){
                    $a->delete();
                }

                return true;
            }            
            elseif($action == 'listadmins')
            {
                $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'exists:App\Domain,domain'],
                ]);
                $domain = Domain::where('domain',$validatedData['domain'])->first();
                if(!Auth::user()->hasRole('admin')){
                    // if not an admin, check if edit roles are enabled for this domain
                    $domain->needRole('edit'); // throws exception if curren user does not have this role
                }
                return $domain->users;

            }
            elseif($action == 'setadmin')
            {
                $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'exists:App\Domain,domain'],
                    'userid' => ['required', 'int', 'exists:App\User,id'],
                    'role' => ['required', 'string','in:own,edit,view'],
                ]);
                $domain = Domain::where('domain',$validatedData['domain'])->first();
                if(!Auth::user()->hasRole('admin')){
                    // if not an admin, check if edit roles are enabled for this domain
                    $domain->needRole('own'); // throws exception if curren user does not have this role
                
                    if($validatedData['userid'] == Auth::user()->id)
                    {
                        throw ValidationException('Cannot edit own ownership');
                    } 
                }

                $domain->users()->attach($validatedData['userid'],['role' => $validatedData['userid']]);
                $domain->save();

                return true;

            }
            elseif($action == 'deladmin')
            {
                $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'exists:App\Domain,domain'],
                    'userid' => ['required', 'int', 'exists:App\User,id'],
                ]);
                $domain = Domain::where('domain',$validatedData['domain'])->first();
                if(!Auth::user()->hasRole('admin')){
                    // if not an admin, check if edit roles are enabled for this domain
                    $domain->needRole('own'); // throws exception if curren user does not have this role

                    if($validatedData['userid'] == Auth::user()->id)
                    {
                        throw ValidationException('Cannot remove own ownership');
                    } 
                }
                $domain->users()->detach($validatedData['userid']);
                $domain->save();

                return true;
            }

            elseif($action == 'listusers')
            {
                $users = User::all();
                return $users;
            }               
            elseif($action == 'list')
            {
                if(Auth::user()->hasRole('admin'))
                {
                    $domains = Domain::all();
                }
                else
                {
                    $domains = Auth::user()->cards;
                }
                return $domains;
            }   
            else
            {
                return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
            }
        }
        catch(ValidationException $v)
        {
            return response(['fail' => 'validation', 'errors' => $v->errors()],400);
        }
        catch(PermissionException $x)
        {
            return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
        }
        catch(ErrorException $v)
        {
            return response(['fail' => 'errors', 'errors' => $v->errors()],400);
        }
    }

    public function tlsAjax(Request $request)
    {
        $action = $request->input('action');
        try
        {
            // allow only admin
            if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}        


            if($action == 'list')
            {
                $policies = TlsPolicy::all();
                return $policies;
            }   
            elseif($action == 'getpolicy')
            {
                $validatedData = $request->validate([
                    'domain' => ['required', 'string',],
                ]);
                $domain = Domain::where('domain',$validatedData['domain'])->first();
                $tlspolicy = $domain->tlspolicy;
                if(empty($tlspolicy)) {
                    return ['policy' => 'null', 'params' => ''];
                } else {
                    return $tlspolicy;
                }
                return response(['fail' => 'action', 'errors' => ["Unauthorized for domain '{$validatedData['domain']}'"]],403);
            }       
            elseif($action == 'setpolicy')
            {
                 $validatedData = $request->validate([
                    'domain' => ['required', 'string', ],
                    'policy' => ['required', 'string','in:null,none,may,encrypt,dane,dane-only,fingerprint,verify,secure'],
                    'params' => ['nullable','string'],
                ]);
            
                $tlspolicy = TlsPolicy::where('domain',$validatedData['domain'])->first();
                if($tlspolicy == null){
                    $tlspolicy = new TlsPolicy(['domain' => $validatedData['domain'],
                                            'policy' => $validatedData['policy'],
                                            'params' => $validatedData['params'] ]);
                    $tlspolicy->save();
                }
                else {

                    $tlspolicy->policy = $validatedData['policy'];
                    $tlspolicy->params = $validatedData['params'];
                    $tlspolicy->save();
                }
                return $tlspolicy;
            }                                
            elseif($action == 'delpolicy')
            {
                 $validatedData = $request->validate([
                    'domain' => ['required', 'string', 'exists:App\TlsPolicy,domain' ],
                ]);
            
                $tlspolicy = TlsPolicy::where('domain',$validatedData['domain'])->first();
                $tlspolicy->delete();
                return true;
            }
            else
            {
                return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
            }
        }
        catch(ValidationException $v)
        {
            return response(['fail' => 'validation', 'errors' => $v->errors()],400);
        }
        catch(PermissionException $x)
        {
            return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
        }
        catch(ErrorException $v)
        {
            return response(['fail' => 'errors', 'errors' => $v->errors()],400);
        }
    }

    public function accountsAjax(Request $request)
    {
        $action = $request->input('action');
        try
        {
            // block unverified users
            if(!(Auth::user()->hasRole('admin') || Auth::user()->hasRole('user'))){
                throw PermissionException('Not Authorized');
            }

            $firstValidatedData = $request->validate([
                'domain' => ['required', 'string', 'exists:App\Domain,domain'],
            ]);
            $domain = Domain::where('domain',$firstValidatedData['domain'])->first();
            if(!Auth::user()->hasRole('admin')){
                // if not an admin, check if edit roles are enabled for this domain
                $domain->needRole('edit'); // throws exception if curren user does not have this role
            }
            if($action == 'create')
            {
                $validatedData = $request->validate([
                    'username' => ['required', 'string',],
                    'enabled' => ['required', 'boolean',],
                    'sendonly' => ['required', 'boolean',],
                    'quota' => ['required', 'integer',],
                    'password' => ['required', 'string',],
                ]);
                
                if(Account::where('username',$validatedData['username'])->where('domain', $firstValidatedData['domain'])->count() > 0){
                    throw new  ValidationException('Account already exists');
                }

                // encode password
                if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
                    throw new  ValidationException('Password is not complex enough');
                }                
                $hash = DovecotPw::Encrypt($validatedData['password']);

                $account = Account::Create([
                    'username' => $validatedData['username'],
                    'domain' => $firstValidatedData['domain'],
                    'enabled' => $validatedData['enabled'],
                    'sendonly' => $validatedData['sendonly'],
                    'quota' => $validatedData['quota'],
                    'password' => $hash,
                ]);
                $account->save();

                return [$account];
            }
            elseif($action == 'checkusername')
            {
                $validatedData = $request->validate([
                    'username' => ['required', 'string', ''],
                    'id' => ['nullable','integer'],
                ]);
                if(!empty($validatedData['id']))
                {
                    $a = Account::Find($validatedData['id']);
                    if($validatedData['username'] == $a->username) {
                        return true;
                    }
                }
                
                return (Account::where('domain',$domain->domain)->where('username',$validatedData['username'])->count() == 0);
            }
            elseif($action == 'list')
            {
                $accounts = $domain->accounts;
                return $accounts;
            }
            elseif($action == 'update')
            {
                $validatedData = $request->validate([
                    'id' => ['required','integer'],
                    'username' => ['nullable', 'string',],
                    'enabled' => ['required', 'boolean',],
                    'sendonly' => ['required', 'boolean',],
                    'quota' => ['required', 'integer',],
                    'password' => ['nullable', 'string',],
                ]);

                $account = Account::Find($validatedData['id']);

                if(!empty($validatedData['username']) && $validatedData['username'] != $account->username)
                {
                    if((Account::where('domain',$domain)->where('username',$validatedData['username'])->count()) == 0)
                    {
                        $account->username = $validatedData['username'];
                    }
                }
                $account->enabled = $validatedData['enabled'];
                $account->sendonly = $validatedData['sendonly'];
                $account->quota = $validatedData['quota'];

                if(!empty($validatedData['password']))
                {
                    if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
                        throw new  ValidationException('Password is not complex enough');
                    }
                    // encode password
                    $hash = DovecotPw::Encrypt($validatedData['password']);
                    $account->password = $hash;
                }
                $account->save();

                return [$account,$hash];
            }
            elseif($action == 'delete')
            {
                $validatedData = $request->validate([
                    'id' => ['required','integer'],
                ]);
                $account = Account::Find($validatedData['id']);
                $account->delete();
                return [];
            }
            else
            {
                return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
            }
        }
        catch(ValidationException $v)
        {
            return response(['fail' => 'validation', 'errors' => $v->errors()],400);
        }
        catch(PermissionException $x)
        {
            return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
        }
        catch(ErrorException $v)
        {
            return response(['fail' => 'errors', 'errors' => $v->errors()],400);
        }
    }

    public function siteAccountsAjax(Request $request)
    {
        $action = $request->input('action');
        try
        {
            // Allow only admin
            if(!(Auth::user()->hasRole('admin'))) { throw PermissionException('Not Authorized');}        

            if($action == 'create')
            {
                $validatedData = $request->validate([
                    'username' => ['required', 'string',],
                    'enabled' => ['required', 'boolean',],
                    'sendonly' => ['required', 'boolean',],
                    'quota' => ['required', 'integer',],
                    'password' => ['required', 'string',],
                ]);
                if(Account::where('username',$validatedData['username'])->where('domain', '')->count() > 0){
                    throw new  ValidationException('Account already exists');
                }

                // encode password
                if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
                    throw new  ValidationException('Password is not complex enough');
                }                
                $hash = DovecotPw::Encrypt($validatedData['password']);

                $account = Account::Create([
                    'username' => $validatedData['username'],
                    'domain' => "localhost",
                    'enabled' => $validatedData['enabled'],
                    'sendonly' => $validatedData['sendonly'],
                    'quota' => $validatedData['quota'],
                    'password' => $hash,
                ]);
                $account->save();

                return [$account];
            }
            elseif($action == 'checkusername')
            {
                $validatedData = $request->validate([
                    'username' => ['required', 'string', ''],
                    'id' => ['nullable','integer'],
                ]);
                if(!empty($validatedData['id']))
                {
                    $a = Account::Find($validatedData['id']);
                    if($validatedData['username'] == $a->username) {
                        return true;
                    }
                }
                
                return (Account::where('domain',"localhost")->where('username',$validatedData['username'])->count() == 0);
            }
            elseif($action == 'list')
            {
                $accounts = Account::where('domain',"localhost")->get();
                return $accounts;
            }
            elseif($action == 'update')
            {
                $validatedData = $request->validate([
                    'id' => ['required','integer'],
                    'username' => ['nullable', 'string',],
                    'enabled' => ['required', 'boolean',],
                    'sendonly' => ['required', 'boolean',],
                    'quota' => ['required', 'integer',],
                    'password' => ['nullable', 'string',],
                ]);

                $account = Account::Find($validatedData['id']);

                if(!empty($validatedData['username']) && $validatedData['username'] != $account->username)
                {
                    if((Account::where('domain',"localhost")->where('username',$validatedData['username'])->count()) == 0)
                    {
                        $account->username = $validatedData['username'];
                    }
                }
                $account->enabled = $validatedData['enabled'];
                $account->sendonly = $validatedData['sendonly'];
                $account->quota = $validatedData['quota'];

                if(!empty($validatedData['password']))
                {
                    if(Entropy::Calculate($validatedData['password']) < static::MinimumEntropy) {
                        throw new  ValidationException('Password is not complex enough');
                    }
                    // encode password
                    $hash = DovecotPw::Encrypt($validatedData['password']);
                    $account->password = $hash;
                }
                $account->save();

                return [$account,$hash];
            }
            elseif($action == 'delete')
            {
                $validatedData = $request->validate([
                    'id' => ['required','integer'],
                ]);
                $account = Account::Find($validatedData['id']);
                $account->delete();
                return [];
            }
            else
            {
                return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
            }
        }
        catch(ValidationException $v)
        {
            return response(['fail' => 'validation', 'errors' => $v->errors()],400);
        }
        catch(PermissionException $x)
        {
            return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
        }
        catch(ErrorException $v)
        {
            return response(['fail' => 'errors', 'errors' => $v->errors()],400);
        }
    }

    private function tidyAliases($aliases)
    {
        // figure out how to merge 

        $a_list = [];

        foreach($aliases as $a)
        {
            if(empty($a_list[$a->source_username]))
            {
                $a_list[$a->source_username] = new \stdClass;
                $a_list[$a->source_username]->source = $a->source_username;
                $a_list[$a->source_username]->dest = [];
            }
            
            $a->destination = empty($a->destination_domain)?($a->destination_username):($a->destination_username.'@'.$a->destination_domain);
            $a_list[$a->source_username]->dest[] = $a;
        }

        return array_values($a_list);
    }

    public function aliasesAjax(Request $request)
    {
        $action = $request->input('action');
        try
        {
            // block unverified users
            if(!(Auth::user()->hasRole('admin') || Auth::user()->hasRole('user'))){
                throw PermissionException('Not Authorized');
            }

            $firstValidatedData = $request->validate([
                'domain' => ['required', 'string', 'exists:App\Domain,domain'],
            ]);
            $domain = Domain::where('domain',$firstValidatedData['domain'])->first();
            if(!Auth::user()->hasRole('admin')){
                // if not an admin, check if edit roles are enabled for this domain
                $domain->needRole('edit'); // throws exception if curren user does not have this role
            }
            if($action == 'checksource')
            {
                $validatedData = $request->validate([
                    'source' => ['required', 'string', ''],
                    'id' => ['nullable','integer'],
                ]);
                if(!empty($validatedData['id']))
                {
                    $a = Alias::Find($validatedData['id']);
                    if($validatedData['source_username'] == $a->source) {
                        return true;
                    }
                }
                return (Alias::where('source_domain',$domain->domain)->where('source_username',$validatedData['source'])->count() == 0);
            }
            elseif($action == 'list')
            {
                return $this->tidyAliases($domain->aliases);
            }
            elseif($action == 'add_dest')
            {
                $validatedData = $request->validate([
                    'source' => ['required','string'],
                    'destination' => ['nullable', 'string',],
                    'enabled' => ['required', 'boolean',],
                    'comment' => ['nullable', 'string',],
                ]);

                $dparts = explode('@',$validatedData['destination'],2);
                $a = Alias::Create([
                    'source_username' => $validatedData['source'],
                    'source_domain' => $domain->domain,
                    'destination_username' => $dparts[0],
                    'destination_domain' => isset($dparts[1])?$dparts[1]:null,
                    'enabled' => $validatedData['enabled'],
                    'comment' => $validatedData['comment'],
                ]);
                $a->save();

                return $this->tidyAliases([$a]);
            }
            elseif($action == 'update_dest')
            {
                $validatedData = $request->validate([
                    'id' => ['required','integer'],
                    'destination' => ['nullable', 'string',],
                    'enabled' => ['required', 'boolean',],
                    'comment' => ['nullable', 'string',],
                ]);

                $a = Alias::Find($validatedData['id']);

                $dparts = explode('@',$validatedData['destination'],2);
                $a->destination_username = $dparts[0];
                $a->destination_domain = isset($dparts[1])?$dparts[1]:null;
                $a->enabled = $validatedData['enabled'];
                $a->comment = $validatedData['comment'];

                $a->save();

                return $this->tidyAliases([$a]);
            }
            elseif($action == 'delete_dest')
            {
                $validatedData = $request->validate([
                    'id' => ['required','integer'],
                ]);
                $a = Alias::Find($validatedData['id']);
                $a->delete();
                return [];
            }
            elseif($action == 'delete_src')
            {
                $validatedData = $request->validate([
                    'source' => ['required','string'],
                ]);
                $aliases = Alias::where('source_domain',$domain->domain)->where('source_username',$validatedData['source']);
                foreach($aliases as $a)
                {
                    $a->delete();
                }
                return [];
            }            
            else
            {
                return response(['fail' => 'action', 'errors' => ['Action unknown: '. $action]],400);
            }
        }
        catch(ValidationException $v)
        {
            return response(['fail' => 'validation', 'errors' => $v->errors()],400);
        }
        catch(PermissionException $x)
        {
            return response(['fail' => 'role', 'errors' => ['Action requires role '. $x->role()]],403);
        }
        catch(ErrorException $v)
        {
            return response(['fail' => 'errors', 'errors' => $v->errors()],400);
        }
    }    
}
