import { Token } from "./Token"
import { gv, type Browser_storage } from "./Storage";
import { Configuration, Mode, Local_user } from "./Configuration";
import {Writable, writable} from 'svelte/store'

export class User
{
    public  given_name      :string = "";
    public family_name      :string = "";
    public picture          :string = "";
    public email            :string = "";
    public email_verified   :boolean = false;
}

export class Header_info
{
    public  key:    string
    public  value:  string
}

export class Tenant_info
{
    public  id:         string
    public  url:        string
    public  name:       string = ""
    public  headers:    Header_info[] | undefined
}

export class App_instance_info
{
    public tenant_id:       string = ""
    public name:            string = ""
    public desc:            string = ""
    public img:             string = ""
    public is_public:       boolean = false;
    public unauthorized_guest_allowed: boolean = false;

}

export class Session
{
    private     my_validation_ticket    :number = 0;
    private     _is_active              :boolean = false;
    private     _user                   :User;
    private     _id_token               :Token;
    private     _access_token           :Token;
    private     _refresh_token          :Token;

    private     storage                 :Browser_storage;

    public      appInstanceInfo         :App_instance_info|null = null;

    public      configuration           :Configuration;
    public      sessionId               :string
    
    constructor(storage: Browser_storage)
    {
        this.storage = storage;
        
        let arr = new Uint8Array((16) / 2)
        window.crypto.getRandomValues(arr)

        const dec2hex = (dec)=> dec.toString(16).padStart(2, "0")
        this.sessionId = Array.from(arr, dec2hex).join('')
    }

    public configure(cfg, internal=false)
    {
        console.log('configure', 'internal', internal, cfg)
        
        this.configuration = new Configuration;

        if(cfg)
        {
            switch(cfg.mode)
            {
            case 'remote':
                this.configuration.mode             = Mode.Remote;
                this.configuration.iss              = cfg.remote.iss;
                this.configuration.client_id        = cfg.remote.client_id ?? cfg.remote.clientID;
                this.configuration.client_secret    = cfg.remote.client_secret ?? cfg.remote.clientSecret;
                this.configuration.scope            = cfg.remote.scope;
                this.configuration.api_version      = cfg.remote.api_version ?? cfg.remote.apiVersion ?? "v001";
                this.configuration.tenant           = cfg.remote.tenant ?? "";
                this.configuration.groups_only      = cfg.remote.groupsOnly ?? cfg.remote.groups_only ?? false;
                this.configuration.ask_organization_name = cfg.remote.ask_organization_name ?? cfg.remote.askOrganizationName ?? true
                this.configuration.refresh_token_persistent  = cfg.remote.refresh_token_persistent ?? cfg.remote.refreshTokenPersistent ?? true;
                this.configuration.terms_and_conditions_href = cfg.remote.terms_and_conditions_href ?? cfg.remote.termsAndConditionsHRef;    
                this.configuration.privacy_policy_href       = cfg.remote.privacy_policy_href ?? cfg.remote.privacyPolicyHRef;
                this.configuration.let_choose_group_first    = cfg.remote.let_choose_group_first ?? cfg.remote.letChooseGroupFirst ?? false; 
                break;

            case 'local':
                this.configuration.mode         = Mode.Local;
                this.configuration.local_api    = cfg.local.api;
                this.configuration.api_version  = cfg.local.api_version ?? cfg.local.apiVersion ?? "v001";

                this.configuration.local_users = [];
                if(cfg.local.users && Array.isArray(cfg.local.users))
                {
                    cfg.local.users.forEach(u => {
                        switch(typeof u)
                        {
                        case 'string':
                            {
                                const user = new Local_user();
                                user.username = u;
                                this.configuration.local_users.push(user);
                            }
                            break;

                        case 'object':
                            {
                                const user = new Local_user();
                                user.username = u.username ?? "";
                                user.role = u.role ?? "";
                                user.groupId = u.groupId ?? 0;
                                user.uid = u.uid ?? 0;
                                this.configuration.local_users.push(user);
                            }
                            break;
                        }
                    });
                }
                break;

            case 'disabled':
                this.configuration.mode         = Mode.Disabled;
                this.configuration.local_api    = cfg.local.api;
                this.configuration.api_version  = cfg.local.api_version ?? cfg.local.apiVersion ?? "v001";
                break;
            }
        }
        else
        {
            this.configuration.mode = Mode.Disabled;
        }

        this.setup_mode(this.configuration.mode);

        if(!internal)
        {
            this.storage.set('_hd_auth_cfg', JSON.stringify(cfg))

            if(this.isValid)  
                this.boost_validation_ticket();
            
            let new_session :Session = new Session(this.storage);
            new_session.clone_from(this);
            session.set(new_session);       // forces store subscribers
        }
    }

    private clone_from(src :Session)
    {
        this.my_validation_ticket   = src.my_validation_ticket;
        this._is_active             = src._is_active;
        this._user                  = src._user;
        this._id_token              = src._id_token;
        this._access_token          = src._access_token;
        this._refresh_token         = src._refresh_token;
        this.configuration          = src.configuration;
    }
    
    public get isActive()  :boolean
    {
        if(!this.isValid)
            this.validate();

        return this._is_active;
    }

    public get user()  :User
    {
        if(!this.isValid)
            this.validate();

        return this._user;
    }

    public get idToken()  :Token
    {
        if(!this.isValid)
            this.validate();

        return this._id_token;
    }

    public get accessToken()  :Token
    {
        if(!this.isValid)
            this.validate();

        return this._access_token;
    }

    public get refreshToken()  :Token
    {
        if(!this.isValid)
            this.validate();

        return this._refresh_token;
    }

    public get isValid() :boolean
    {
        let ticket :number;
        if(!this.storage.get_num("_hd_auth_session_validation_ticket", (v) => {ticket = v;}))
            return false;

        return (ticket == this.my_validation_ticket);
    }

    public get apiAddress()    :string
    {
        let res :string;
        if(this.storage.get("_hd_auth_api_address", (v)=>{res=v;}))
            return res;
        else
            return "";
    }


    public get tid()    :string
    {
        let res :string;
        if(this.storage.get("_hd_auth_tenant", (v)=>{res=v;}))
            return res;
        else
            return "";
    }

    public get appId()  :string
    {
        let scopes = this.configuration.scope.split(' ')
        if(!scopes.length)
            return '';
        
        //remove predefined scopes
        scopes = scopes.filter( s => (s!='openid') && (s!='profile') && (s!='email') && (s!='address') && (s!='phone'));
        if(!scopes.length)
            return '';

        let app_id = scopes[0];
        return app_id;
    }

    public get tenants():   Tenant_info[]
    {
        let res: string;
        if(!this.storage.get("_hd_signedin_tenants", (v)=>{res=v;}))
            return [];
        if(!res)
            return [];

        const tenants : Tenant_info[] = JSON.parse(res);
        return tenants;
    }

    public set tenants(infos: Tenant_info[])
    {
        const tInfos = JSON.stringify(infos)
        this.storage.set("_hd_signedin_tenants", tInfos, false);
    }

    public get isUnauthorizedGuest() :boolean
    {
        let result: boolean = false;
        
        let res: string;
        if(!this.storage.get("_hd_auth_unauthorized_guest", (v)=>{res=v;}))
            result = false;
        else if(res == "1")
            result = true;
        else
            result = false;

        return result;
    }

    public set isUnauthorizedGuest(val :boolean) 
    {
        this.storage.set("_hd_auth_unauthorized_guest", val ? "1" : "", true);
    }

    protected validate() : void
    {
        if(!this.storage.get_num("_hd_auth_session_validation_ticket", (v) => {this.my_validation_ticket = v;}))
        {
            this.my_validation_ticket = 1;
            this.storage.set_num("_hd_auth_session_validation_ticket", this.my_validation_ticket);
        }

        if(!this.configuration)
        {
            let cfg_json;
            if(this.storage.get('_hd_auth_cfg', (v) => cfg_json = v))
            {
                try
                {
                    let cfg = JSON.parse(cfg_json);
                    this.configure(cfg, true);
                }
                catch(err)
                {
                    console.error(err);
                }
            }
        }

        if(this.disabled)
        {
            this.setCurrentTenantAPI(this.configuration.local_api, '')
            this._is_active = true;
            return;
        }
        else if(this.local)
        {
            if(this.localDevCurrentUser)
            {
                this._is_active = true;
            }
            else
            {
                this._is_active = false;
            }

            this.setCurrentTenantAPI(this.configuration.local_api, '')
            return;
        }

        this._is_active = false;

        let token :string;
        if(this.storage.get("_hd_auth_id_token", (v) => {token=v;}))
        {
            this._id_token = new Token(token);
            this._user = new User();
            this._user.given_name       = this._id_token.get_claim<string>("given_name");
            this._user.family_name      = this._id_token.get_claim<string>("family_name");
            this._user.picture          = this._id_token.get_claim<string>("picture");
            this._user.email            = this._id_token.get_claim<string>("email");
            this._user.email_verified   = this._id_token.get_claim<boolean>("email_verified");
        }
        else
            this._id_token = null;


        if(this.storage.get("_hd_auth_access_token", (v) => {token=v;}))
            this._access_token = new Token(token);
        else
            this._access_token = null;


        if(this.storage.get("_hd_auth_refresh_token", (v) => {token=v;}))
            this._refresh_token = new Token(token, false);
        else
            this._refresh_token = null;

        if((this._access_token != null) || (this._id_token != null))
            this._is_active = true;
    }

    public refreshTokens(tokens_info, chosen_tenant_id = undefined): boolean
    {
        if(!tokens_info.access_token)
            return false;
    
        if(!tokens_info.id_token)
            return false;
            
        if(!tokens_info.refresh_token)
            return false;

        this.storage.set("_hd_auth_id_token", tokens_info.id_token);
        this.storage.set("_hd_auth_access_token", tokens_info.access_token);
        this.storage.set("_hd_auth_refresh_token", tokens_info.refresh_token, this.configuration.refresh_token_persistent);

        this._id_token = new Token(tokens_info.id_token);
        this._user = new User();
        this._user.given_name       = this._id_token.get_claim<string>("given_name");
        this._user.family_name      = this._id_token.get_claim<string>("family_name");
        this._user.picture          = this._id_token.get_claim<string>("picture");
        this._user.email            = this._id_token.get_claim<string>("email");
        this._user.email_verified   = this._id_token.get_claim<boolean>("email_verified");

        this._access_token = new Token(tokens_info.access_token);
        this._refresh_token = new Token(tokens_info.refresh_token, false); 
        this._is_active = true;

        if(tokens_info.tenant != undefined)
        {
            this.setCurrentTenantAPI(tokens_info.tenant.url, tokens_info.tenant.id);
            this.tenants = [tokens_info.tenant];
        }
        else if((tokens_info.tenants != undefined) && (tokens_info.tenants.length > 0))
        {
            if(tokens_info.tenants.length == 1)
                this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
            else
            {
                if(chosen_tenant_id)
                {
                    const chosen_tenant = tokens_info.tenants.find( el => el.id == chosen_tenant_id)
                    if(chosen_tenant)
                        this.setCurrentTenantAPI(chosen_tenant.url, chosen_tenant.id);
                    else
                        this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
                }
                else
                    this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
            }

            this.tenants = tokens_info.tenants
        }
        else
            return false;


        return true;
    }

    public signin(tokens_info, chosen_tenant_id = undefined) :boolean
    {
        if((tokens_info.access_token == undefined) || (tokens_info.access_token == ""))
        {
            this.signout();
            return true;
        }

        if((tokens_info.id_token == undefined) || (tokens_info.id_token == ""))
        {
            this.signout();
            return true;
        }

        if((tokens_info.refresh_token == undefined) || (tokens_info.refresh_token == ""))
        {
            this.signout();
            return true;
        }

        this.storage.set("_hd_auth_access_token", tokens_info.access_token);
        this.storage.set("_hd_auth_id_token", tokens_info.id_token);
        this.storage.set("_hd_auth_refresh_token", tokens_info.refresh_token, this.configuration.refresh_token_persistent);

        if(tokens_info.tenant != undefined)
        {
            this.setCurrentTenantAPI(tokens_info.tenant.url, tokens_info.tenant.id);
            this.tenants = [tokens_info.tenant];
        }
        else if((tokens_info.tenants != undefined) && (tokens_info.tenants.length > 0))
        {
            if(tokens_info.tenants.length == 1)
                this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
            else
            {
                if(chosen_tenant_id)
                {
                    const chosen_tenant = tokens_info.tenants.find( el => el.id == chosen_tenant_id)
                    if(chosen_tenant)
                        this.setCurrentTenantAPI(chosen_tenant.url, chosen_tenant.id);
                    else
                        this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
                }
                else
                    this.setCurrentTenantAPI(tokens_info.tenants[0].url, tokens_info.tenants[0].id);
            }

            this.tenants = tokens_info.tenants
        }
        else if((tokens_info.apps != undefined) && (tokens_info.apps.length > 0))
        {
            // todo: multi app not supported yet?
            this.signout();
            return false;
        }
        else
        {
            this.signout();
            return false;
        }

        this.boost_validation_ticket();
        this.validate();

        this.checkServerAndClientTimeMismatch();

        let new_session :Session = new Session(this.storage);
        new_session.clone_from(this);
        session.set(new_session);       // forces store subscribers
        
        return true;
    }

    protected checkServerAndClientTimeMismatch() :void
    {
        if(!this._access_token)
            return;

        const serverTime = this._access_token.get_claim<number>("iat");
        if(!serverTime)
            return;

        const clientTime = Math.floor(Date.now() / 1000);
        const timeShift = clientTime - serverTime;
        
        // now just logging. In the near future we need to store this value and use on Token::not_expired property
        console.log('Server/Client time mismatch: ', timeShift);
    }

    protected boost_validation_ticket() :void
    {
        let validation_ticket :number = 0;
        this.storage.get_num("_hd_auth_session_validation_ticket", (v) => {validation_ticket = v;});
        validation_ticket++;
        this.storage.set_num("_hd_auth_session_validation_ticket", validation_ticket);
        this.my_validation_ticket = validation_ticket;
    }

    public setCurrentTenantAPI(url :string, tid :string) :void
    {
        this.storage.set("_hd_auth_api_address", url);
        this.storage.set("_hd_auth_tenant", tid);
        this.storage.set('_hd_auth_last_chosen_tenant_id', tid, true);
    }

    public get lastChosenTenantId() :string
    {
        let res;
        if(!this.storage.get('_hd_auth_last_chosen_tenant_id', (v) => res=v))
            return '';
        
        return res;
    }

    public signout() : void
    {
        this.storage.set("_hd_auth_id_token", "");
        this.storage.set("_hd_auth_access_token", "");
        this.storage.set("_hd_auth_refresh_token", "", this.configuration.refresh_token_persistent);

        this.storage.set("_hd_auth_api_address", "");
        this.storage.set("_hd_auth_tenant", "");
       
        this.storage.set("_hd_auth_local_dev_user", "");
        this.storage.set('_hd_auth_unauthorized_guest', "", true)

        this._id_token = null;
        this._access_token = null;
        this._refresh_token = null;
        this._is_active = false;

        this.boost_validation_ticket();

        let new_session :Session = new Session(this.storage);
        new_session.clone_from(this);
        session.set(new_session);       // forces store subscribers
    }

    public appAccessRole() : string
    {
        if(!this.configuration)
            return '';
        
        const scopes = this.configuration.scope.split(' ')
        if((!scopes) || scopes.length == 0)
            return '';

        const appId = scopes[scopes.length-1];

        if(!this.isActive)
            return '';

        const token: Token = this.accessToken;
        if(token == undefined)
            return '';

        if(token == null)
            return '';

        if(!token.raw)
            return '';

        if(!token.is_jwt)
            return '';

        const access: object[] = token.payload['access'];

        if( !!access && 
            access.length > 0)
        {
            const scopeIdx = access.findIndex(e => e['app'] == appId)
            if(scopeIdx < 0)
                return '';

            const accessScope: object =  access[scopeIdx];

            const scopeTenants = accessScope['tenants'];
            if(!scopeTenants || scopeTenants.length == 0)
                return '';

            for(let i=0; i<scopeTenants.length; i++)
            {
                const tenantInfo = scopeTenants[i];
                if(typeof tenantInfo === 'object' && tenantInfo !== null)
                {
                    if(tenantInfo['tid'] == this.tid)
                    {
                        if(!tenantInfo.details)
                            return '';

                        const accessDetails = JSON.parse(tenantInfo.details);
                        return accessDetails.role ?? '';
                    }
                }
            }
            return '';
        }
        else
            return '';
    }

    public authAccessGroup() : number
    {
        return this.accessGroup("auth");
    }

    public filesAccessGroup() : number
    {
        return this.accessGroup("files");
    }

    private accessGroup(scope: string) : number
    {
        if(!this.isActive)
            return 0;

        const token: Token = this.accessToken;
        if(token == undefined)
            return 0;

        if(token == null)
            return 0;

        if(!token.raw)
            return 0;

        if(!token.is_jwt)
            return 0;

        const access: object[] = token.payload['access'];

        if( !!access && 
            access.length > 0)
        {
            const scopeIdx = access.findIndex(e => e['app'] == scope)
            if(scopeIdx < 0)
                return 0;

            const accessScope: object =  access[scopeIdx];

            const scopeTenants = accessScope['tenants'];
            if(!scopeTenants || scopeTenants.length == 0)
                return 0;

            for(let i=0; i<scopeTenants.length; i++)
            {
                const tenantInfo = scopeTenants[i];
                if(typeof tenantInfo === 'object' && tenantInfo !== null)
                {
                    if(tenantInfo['tid'] == this.tid)
                        return tenantInfo['gid'] ?? 0;
                }
            }
            return 0;
        }
        else
            return 0;

    }

    public async __is_admin() :Promise<boolean>
    {
        if(!this.isValid)
            this.validate();

        if(!this.isActive)
            return false;

        if(this.tid == "")
            return false;

        let path :string;
        path = this.configuration.iss + "/auth/am_i_admin";
        path += "?tenant=" + this.tid;

        const  res = await fetch(  path,
                                {
                                    method: 'get',
                                    headers : new Headers({
                                        'Authorization':'Bearer ' + this._access_token.raw,
                                        'Accept': 'application/json'})
                                });
        if(!res.ok)
            return false;

        const result = await res.json();
        return result.response === true;
    }

    public get mode() :Mode
    {
        let num_mode :number = 0;
        if(!this.storage.get_num('_hd_auth_session_mode', (v) => { num_mode = v; }))
            return Mode.Remote;
        else switch(num_mode)
        {
        case 0:
            return Mode.Remote;

        case 1:
            return Mode.Local;

        case 2:
            return Mode.Disabled;

        default:
            return Mode.Remote;
        }
    }

    public set mode( m :Mode)
    {
        switch(m)
        {
        case Mode.Remote:
            this.storage.set_num("_hd_auth_session_mode", 0);
            break;

        case Mode.Local:
            this.storage.set_num("_hd_auth_session_mode", 1);
            break;

        case Mode.Disabled:
            this.storage.set_num("_hd_auth_session_mode", 2);
            break;
        }

    }

    public get remote() :boolean
    {
        return (this.mode == Mode.Remote);
    }

    public get local() :boolean
    {
        return (this.mode == Mode.Local);
    }

    public get disabled() :boolean
    {
        return (this.mode == Mode.Disabled);
    }

    protected setup_mode(m :Mode)
    {
        let was_remote :boolean = this.mode == Mode.Remote;
        
        //this.signout();
        this.mode = m;

       
        if(m==Mode.Remote)
        {
            /*let org_api_addr :string;
            if(this.storage.get("_hd_auth_org_api_address", (v)=>{org_api_addr=v;}))
            {
                this.storage.set("_hd_auth_api_address", org_api_addr);
                this.storage.set("_hd_auth_org_api_address", '');  
            }
            */
        }
        else
        {
            if(this.configuration && this.configuration.local_api)
            {   
                //this.storage.set("_hd_auth_org_api_address", '');  
                //this.storage.set("_hd_auth_api_address", this.configuration.local_api);  
                //this.setCurrentTenantAPI(this.configuration.local_api, '')
            }
        }
    }

    public setLocalDevCurrentUser(email :string)
    {
        this.storage.set('_hd_auth_local_dev_user', email);

        this.boost_validation_ticket();
        this.validate();

        let new_session :Session = new Session(this.storage);
        new_session.clone_from(this);
        session.set(new_session);       // forces store subscribers
        
    }

    public get localDevCurrentUser() :Local_user
    {
        let email :string;
        this.storage.get('_hd_auth_local_dev_user', (v)=>{email=v;});

        if(!email)
            return null;

        const foundUser = this.configuration.local_users.find(u => u.username == email)
        return foundUser;
    }

}

export const session :Writable<Session> = writable(new Session(gv));