export type Entry={id:string;kind:'project'|'employee';team:string;payload:any;revision:number;updatedAt?:string;updatedBy?:string};
export type Config={teams:string[];weeks:string[];workingDays:Record<string,number>;lists:Record<string,string[]>;drawingHours?:Record<string,number>;retiredLists?:Record<string,string[]>;teamResponsibles?:Record<string,string[]>;statusEffects?:Record<string,string>;clientMapping:Record<string,string>};
export const WORK_TYPES=['Design','CAD','Rebar','Steel','Arch'];
export const SERIES_COLORS={capacity:'#4B78A8',planned:'#009999',required:'#E3913C'};
export const measures=['fte','requiredFte','capacity','planned','required','delay','modifications'] as const;
export const labels:Record<string,string>={employeeCount:'Employee count',balance:'Balance FTE',fte:'Available FTE',requiredFte:'Required FTE',capacity:'Manhour capacity',planned:'Planned manhours',required:'Required manhours',delay:'Delay',modifications:'Modifications'};
export function currentWeek(date=new Date()){
 const parts=new Intl.DateTimeFormat('en-CA',{timeZone:'Asia/Kolkata',year:'numeric',month:'numeric',day:'numeric'}).formatToParts(date);
 const part=(key:string)=>Number(parts.find(p=>p.type===key)!.value);
 const d=new Date(Date.UTC(part('year'),part('month')-1,part('day')));
 d.setUTCDate(d.getUTCDate()+4-(d.getUTCDay()||7));
 const year=d.getUTCFullYear(),first=new Date(Date.UTC(year,0,1));
 const week=Math.ceil(((d.getTime()-first.getTime())/86400000+1)/7);
 return String(year).slice(-2)+'W'+String(week).padStart(2,'0');
}
export function defaultStartWeek(weeks?:string[],date=new Date()){
 const current=currentWeek(date);if(!weeks?.length||weeks.includes(current))return current;
 return weeks.find(w=>w>=current)||weeks.at(-1)!;
}
export function capacityColour(requiredFte:number,fte:number){
 if(!Number.isFinite(requiredFte)||!Number.isFinite(fte)||(requiredFte===0&&fte===0))return undefined;
 const diff=Math.max(-5,Math.min(5,requiredFte-fte));
 const a=diff<=0?[0,224,0]:[255,170,0],b=diff<=0?[255,255,0]:[255,0,0],t=diff<=0?(diff+5)/5:diff/5;
 return '#'+a.map((v,i)=>Math.round(v+(b[i]-v)*t).toString(16).padStart(2,'0')).join('');
}
export const planningNotes=[
 'Planned manhours include rows with blank or Progressing status.',
 'Required manhours = Planned manhours − Delay + Modifications.',
 'Available FTE = 0.9 × sum of employee productivity indices.',
 'Manhour capacity = Available FTE × 8 × number of working days.',
 'Required FTE = Required manhours ÷ (8 × number of working days).',
 'Team Capacity colours use Required FTE − FTE: −5 or below is green; zero is yellow; positive differences grade from orange to red at +5 or above.',
 'Weekly Software combines all status entries. Software names follow the Field lists.',
 'The default start week is the current ISO week in India. Earlier data remains available from 26W40. If the current week is outside the available range, the nearest available week is selected.',
 'Office reports refresh every five seconds while no input draft is open. Click Save input to apply changes.',
 'FTE graphs use Available FTE (0.9 × sum of productivity indices) and Required FTE from the reports. Planned FTE = planned manhours ÷ (8 × working days).',
 'Balance FTE = Available FTE − Required FTE, calculated for each category and work type; negative balances are displayed with a light red highlight. All-categories balances use combined available and required values.',
 'Hide excludes weeks from its first excluded week onward; earlier weeks remain valid in reports, graphs and exports. Delay is deducted in filtered subtotals.',
 'Drawing estimates = corresponding manhours ÷ manhours per drawing. Project selections combine rows with the same client and project name, including planned, delay and modification lines. Drawing values are estimates and may be fractional.',
 'Arch includes CAD-RRN and CAD-SOS. Shejina’s transfer preserves earlier productivity under her original team.',
 'SWECO category selections filter required hours and Required FTE; available capacity is shown as a line only with Select all categories. Drawing charts stack required drawings by selected project category, with a net total above each week.',
 'Team-wise Balance FTE uses each team’s available minus required FTE and includes negative balances with a light red highlight.',
 'Cell check highlights are applied and removed manually in Team inputs. They are shared without reviewer names. Changing a checked value or its calculation details clears that check.',
 'Preferred team is saved in the current browser. Employee order is saved centrally for the team.',
 'Employee count counts each name once per team/week if any active productivity entry is nonzero. Name matching ignores case and extra spaces; totals sum these team counts. Productivity and FTE still sum all valid entries. Hide cutoffs apply. Expected projects contribute requirements without adding available capacity. Confirmed projects can be transferred with all their related lines and weeks.',
 'Graphs and summaries recalculate from project inputs, employee productivity indices and working days.'
];
export function categoryFor(client:string,config?:Config){return ({sweco:'SWECO',enigmatix:'Enigmatix'} as Record<string,string>)[String(client||'').trim().toLowerCase()]||'Others';}
export function summarize(records:Entry[],config:Config,weeks:string[],filters:{workType?:string;clientCategory?:string;team?:string;projectCategory?:string;unlistedProjectCategory?:boolean;software?:string}={}){
 return weeks.map(week=>{const employees=new Set<string>();let fte=0,planned=0,delay=0,modifications=0;for(const r of records){const p=r.payload;if(!activeInWeek(r,week))continue;if(filters.team&&r.team!==filters.team||filters.workType&&p.workType?.toLowerCase()!==filters.workType.toLowerCase()||filters.clientCategory&&(p.clientCategory||'Unassigned')!==filters.clientCategory)continue;
 if(r.kind==='employee'){if(!filters.projectCategory&&!filters.unlistedProjectCategory&&!filters.software){const value=Number(p.weeks[week]||0);fte+=value;if(value!==0){const name=String(p.name||'').normalize('NFKC').trim().replace(/\s+/g,' ').toLowerCase();employees.add(JSON.stringify([r.team,name||r.id]));}}continue;}
 if(filters.unlistedProjectCategory&&projectCategories(config).includes(p.category))continue;
 if(filters.projectCategory&&(p.category||'Unassigned')!==filters.projectCategory||filters.software&&(p.software||'Unassigned')!==filters.software)continue;
 const h=Number(p.weeks[week]||0),status=statusEffect(p.status,config);if(status==='planned')planned+=h;else if(status==='delay')delay+=h;else if(status==='modifications')modifications+=h;
 }const days=Number(config.workingDays[week]||0),required=planned-delay+modifications;fte*=0.9;return {week,employeeCount:employees.size,fte,requiredFte:days?required/(8*days):0,capacity:fte*8*days,planned,required,delay,modifications};});
}
export function projectCategorySummary(records:Entry[],config:Config,weeks:string[],filters:{workType?:string;clientCategory?:string}={}){
 return [{name:'Category blank / not listed',data:summarize(records,config,weeks,{...filters,unlistedProjectCategory:true})},...projectCategories(config).filter(Boolean).map(category=>({name:category,data:summarize(records,config,weeks,{...filters,projectCategory:category})}))];
}
export function canEdit(user:any,team:string){return user.role==='admin'||user.role==='editor'&&user.teams.includes(team);}
export function createProjectLine(source:Entry){if(source.kind!=='project')throw new Error('Additional lines belong to project inputs.');return {kind:'project' as const,team:source.team,revision:0,payload:{...structuredClone(source.payload),status:'Progressing',weeks:{},lineParentId:source.payload.lineParentId||source.id}};}
export function orderProjectLines(rows:Entry[]){const ids=new Set(rows.map(r=>r.id));const children=new Map<string,Entry[]>();for(const row of rows){const parent=row.payload.lineParentId;if(parent&&parent!==row.id&&ids.has(parent)){if(!children.has(parent))children.set(parent,[]);children.get(parent)!.push(row);}}const output:Entry[]=[];const seen=new Set<string>();function append(row:Entry){if(seen.has(row.id))return;seen.add(row.id);output.push(row);for(const child of children.get(row.id)||[])append(child);}for(const row of rows)if(!row.payload.lineParentId||!ids.has(row.payload.lineParentId))append(row);for(const row of rows)append(row);return output;}

export function statusEffect(status:string,config:Config){return !status?'planned':config.statusEffects?.[status]||({progressing:'planned',delay:'delay',modifications:'modifications'} as any)[status.toLowerCase()]||'ignore';}
export function inputValue(row:Entry,week:string,config:Config){if(!activeInWeek(row,week))return 0;return Number(row.payload.weeks[week]||0)*(row.kind==='project'&&statusEffect(row.payload.status,config)==='delay'?-1:1);}
export function rowPeriodTotal(row:Entry,config:Config,future:boolean){const current=currentWeek();return config.weeks.filter(w=>future?w>=current:w<current).reduce((n,w)=>n+inputValue(row,w,config),0);}
export function graphValues(data:any[],config:Config,fte:boolean){return data.map(d=>fte?{...d,capacity:d.fte,planned:config.workingDays[d.week]?d.planned/(8*config.workingDays[d.week]):0,required:d.requiredFte}:{...d});}
export function weekRange(config:Config,from:string,to?:string){const i=Math.max(0,config.weeks.indexOf(from));return config.weeks.slice(i,to?Math.max(i,config.weeks.indexOf(to))+1:i+13);}

export function balanceGroups(records:Entry[],config:Config,weeks:string[],clientCategory=''){
 return ['', ...WORK_TYPES].map(workType=>({name:workType||'Total',data:summarize(records,config,weeks,{...(clientCategory?{clientCategory}:{}),...(workType?{workType}:{})}).map(d=>({...d,balance:d.fte-d.requiredFte}))}));
}

export function activeInWeek(row:Entry,week:string){return !row.payload.hidden||!!row.payload.hideFromWeek&&week<row.payload.hideFromWeek;}
export function teamBalanceGroups(records:Entry[],config:Config,weeks:string[],clientCategory=''){
 return [...new Set([...config.teams,...records.map(r=>r.team)])].map(team=>({name:team,data:summarize(records,config,weeks,{team,...(clientCategory?{clientCategory}:{})}).map(d=>({...d,balance:d.fte-d.requiredFte}))}));
}
export function selectedCategoryData(records:Entry[],config:Config,weeks:string[],selected:string[]|null,workType='',clientCategory='SWECO'){
 const groups=projectCategorySummary(records,config,weeks,{...(workType?{workType}:{}),...(clientCategory?{clientCategory}:{})}).filter(g=>selected===null||selected.includes(g.name));
 const total=weeks.map((week,i)=>({week,required:groups.reduce((n,g)=>n+g.data[i].required,0),requiredFte:groups.reduce((n,g)=>n+g.data[i].requiredFte,0)}));
 return {groups,total};
}
export function projectCategories(config:Config){return Array.from(new Set([...config.lists.categories,...(config.retiredLists?.categories||[])]));}
export function projectKey(row:Entry){return JSON.stringify([String(row.payload.client||'').trim().toLowerCase(),String(row.payload.project||'').trim().toLowerCase()]);}
export function projectCatalog(records:Entry[],workType='',clientCategory=''){
 const map=new Map<string,{id:string;name:string;client:string}>();
 for(const r of records)if(r.kind==='project'&&String(r.payload.project||'').trim()&&(!workType||r.payload.workType===workType)&&(!clientCategory||r.payload.clientCategory===clientCategory)){const id=projectKey(r);if(!map.has(id))map.set(id,{id,name:r.payload.project.trim(),client:r.payload.client||'Unspecified client'});}
 return [...map.values()].sort((a,b)=>a.name.localeCompare(b.name)||a.client.localeCompare(b.client));
}
export function selectedProjectData(records:Entry[],config:Config,weeks:string[],ids:string[],workType='',clientCategory=''){
 const scope=records.filter(r=>r.kind==='project'&&(!workType||r.payload.workType===workType)&&(!clientCategory||r.payload.clientCategory===clientCategory));
 const catalog=projectCatalog(scope);const selected=new Set(ids);
 const projects=catalog.filter(p=>selected.has(p.id)).map(p=>({...p,data:summarize(scope.filter(r=>projectKey(r)===p.id),config,weeks)}));
 const total=summarize(scope.filter(r=>selected.has(projectKey(r))),config,weeks);
 return {projects,total};
}

export function swecoGraphData(records:Entry[],config:Config,weeks:string[],selected:string[]|null,workType=''){
 const required=selectedCategoryData(records,config,weeks,selected,workType).total;
 return summarize(records,config,weeks,{clientCategory:'SWECO',...(workType?{workType}:{})}).map((d,i)=>({...d,...required[i]}));
}

export function teamWorkType(team:string){if(team.startsWith('Expected · '))return team.slice(11);return ['CAD-RRN','CAD-SOS'].includes(team)?'Arch':team==='DES-RPM'?'Rebar':team.startsWith('REB')?'Rebar':team.startsWith('CAD')?'CAD':team.startsWith('DES')?'Design':team.startsWith('STL')||team.startsWith('Steel')?'Steel':'';}
export function projectDefaults(team:string){const type=teamWorkType(team),workType=type==='Arch'&&!team.startsWith('Expected · ')?'CAD':type,client=type==='Steel'?'CaddAlta':'SWECO';return {workType,client,clientCategory:categoryFor(client),software:({Rebar:'Strakon',CAD:'AutoCAD',Arch:'AutoCAD',Steel:'Tekla',Design:'Plaxis'} as any)[type]||''};}
export function employeeOrder(rows:Entry[]){return [...rows].sort((a,b)=>(a.payload.sortOrder??Number.MAX_SAFE_INTEGER)-(b.payload.sortOrder??Number.MAX_SAFE_INTEGER)||String(a.payload.name||'').localeCompare(String(b.payload.name||''))||a.id.localeCompare(b.id));}

export function teamCapacitySeries(data:ReturnType<typeof summarize>,fte:boolean,clientCategory=''){
 return [...(!clientCategory?[{id:'available',name:fte?'Available FTE':'Manhour capacity',color:SERIES_COLORS.capacity,line:true,values:data.map(d=>fte?d.fte:d.capacity)}]:[]),...(!clientCategory&&fte?[{id:'employees',name:'Employee count',color:'#159447',line:true,values:data.map(d=>d.employeeCount)}]:[]),{id:'required',name:fte?'Required FTE':'Required manhours',color:SERIES_COLORS.required,values:data.map(d=>fte?d.requiredFte:d.required)}];
}
